# Overview Limit Break has dedicated itself to building public goods that are pro-creator and pro-business to help the Web3 ecosystem thrive. To that end, Limit Break is pleased to announce the release of the Creator Advanced Protection Suite (CAPS). CAPS is a set of open-sourced, independent smart contract products that, when used properly, offer safeguards not only to token protocols but numerous other types of non-token protocols. Using CAPS, creators are able to carve out their own private on-chain ecosystem that is deployable on any EVM chain. - **Creator Token Standards** offers creator-defined guardrails to the transfer function of ERC20, ERC721, and ERC1155 tokens. In its simplest form, this allows creators to decide what protocols can be used to transfer NFTs and other coins, whether it be for trading, on-chain games, or other custom use cases involving the transfer of tokens. - **Payment Processor** offers the first NFT trading protocol that puts creators first over exchanges. Not only does the protocol guarantee payment of royalties in a dynamic manner, it puts the creator in control over how their collections trade. It even includes mechanisms that can align incentives between creators and exchanges, making for a more equitable relationship. - **TokenMaster** offers a next-generation fungible token protocol optimized for onchain consumer economies. It includes the ability to deploy, buy, sell and spend ERC20-C apptokens. - **Trusted Forwarder** offers an attribution system for open on-chain protocols with the option to include protection to gate-keep Web2 applications that are permitted to interact with on-chain protocols. Creator Token Contracts, Payment Processor, and Trusted Forwarder are all independent products, but they all work together in a layered approach to bestow creators with security for their on-chain works. The relationship between these products is depicted in the diagram below. To recap, creators can use CAPS to: - Block all protocols from interacting with your works without permission! - Launch coins and tokens with confidence! - Enforce royalty payments and/or coin-related fees! - Add application-level attribution to open, on-chain protocols! Want to learn the specifics? The following guides will walk you through everything you need to know to implement CAPS. - [Creator Token Standards](./creator-token-standards/overview) - [Payment Processor](./payment-processor/overview) - [TokenMaster](./token-master/overview) - [Trusted Forwarder](./trusted-forwarder/overview) ## Current Contract Addresses ### Public Infrastructure Contracts | Contract | Address | | ------------------------------------------------ | -------------------------------------------- | | Wrapped Native | `0x6000030000842044000077551D00cfc6b4005900` | | EOA Registry | `0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C` | | Creator Token Transfer Validator / Permit-C (v4) | `0x721C002B0059009a671D00aD1700c9748146cd1B` | | Trusted Forwarder Factory | `0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE` | | Collection Settings Registry (Payment Processor) | `0x9A1D001A842c5e6C74b33F2aeEdEc07F0Cb20BC4` | | Payment Processor Encoder (v3) | `0x9A1D00C3a699f491037745393a0592AC6b62421D` | | Payment Processor (v3) | `0x9a1D00000000fC540e2000560054812452eB5366` | | TokenMaster Router | `0x0E00009d00d1000069ed00A908e00081F5006008` | ### Internal Infrastructure Contracts | Contract | Address | | ---------------- | -------------------------------------------- | | Multisig Factory | `0x0000001Bd9DA29d107c93C8087Dd0365953DD0eA` | | Role Server | `0x00000000B77AE1Fe200078D60F382e962f472E23` | | Role Set Server | `0x00000000d7b37203F54e165Fb204B57c30d15835` | # Apptokens Cookbook Apptokens unlock an infinite number of new token types by adding programmable features into the token smart contract. With **apptokens**, developers have a complete suite of tools they need to design tokens that are functionally effective at serving all of their needs. This document aims to outline a list of potential new token types, their features, and the business problems they solve. The list is non-exhaustive as developers will continue to invent new designs, features, and primitives using **apptokens**. ## Categories 1. [Client-side Apptokens](#client-side-apptokens) 2. [Trade Apptokens](#trade-apptokens) 3. [Temporal Apptokens](#temporal-apptokens) 4. [Spend-only Apptokens](#spend-only-apptokens) 5. [Compliant Apptokens](#compliant-apptokens) 6. [Backed Apptokens](#backed-apptokens) --- ## Client-side Apptokens Client-side **apptokens** are designed to secure tokens into a **web2 client-side environment**. **Client-side apptokens** are available for trading and spending when users are interacting with them inside of a **developer-specified client environment**. ### What new opportunities do client-side apptokens unlock? 1. [Maximized token-driven user acquisition](#user-acquisition) 2. [Augmented quest/task completion incentives](#quests) 3. [Enhanced token security for developers and users](#security) 4. [Novel offerwall products generating ad revenue](#offerwall) --- # Maximize token-driven user acquisition ### How can client-side apptokens maximize conversion from token-driven user acquisition? Tokens are the most powerful source of traffic for crypto applications. Applications have been able to use tokens to grow rapidly at an extremely low cost relative to their web2 counterparts. **Client-side apptokens** provide an enormous upgrade to token-driven user acquisition by encoding the application sign-up/install requirement into the token contract itself. **Client-side apptokens** synchronize the demand for a token with the sign-ups/installs of an application, and maximize conversion from token demand to 100%. For example: # - There is enormous demand for a game developer's token launch. - The game developer releases their first title, and wishes to drive the demand for their token into their game. - The token is designed as a **client-side apptoken** and is available for purchase/claim inside of the game's application. - The token purchase/claim goes live, and all of the demand for the token is converted into game installs. # Augment quest/task completion incentives ### How can client-side apptokens boost quest/task completion incentives for applications? Token-driven incentives are often used by crypto applications in order to drive specific task completion. Applications will offer users the chance to win free tokens in order to incentivize them through a funnel of tasks. Often these tasks drive critical interactions for the application. **Client-side apptokens** can be used to incentivize these tasks natively in the token contract by requiring task completion in order to interact with a token. Currently, token incentive platforms struggle to enforce conversion of tasks as the task is disconnected from the token itself. For example: # - Game developer notices that once players hit level 5 of their game, their monthly retention rate triples. - Game developer wishes to use demand for an in-game token to drive players to hit the level 5 milestone. - The token is programmed as a **client-side apptoken** which requires players to complete level 5 of the game in order to purchase and trade the token. - All of the demand for the token is now converted into players completing level 5 of the game and the monthly retention rate of the game jumps. # Enhance security for tokens ### How can client-side apptokens enhance token security for developers and users? **Client-side apptokens** secure all token interactions with **web2 accounts** infrastructure. This means developers can provide an enhanced experience for users with less technical knowledge of web3 security practices such as seed phrases and private key management. By requiring a signature from an application for every interaction, **client-side apptokens** drill web2 account security all the way into the token contract itself. **Client-side apptokens** can be secured by multi-factor authentication the same way the most secure web2 applications are. For example: # - Users signed up to a memecoin platform are required to secure their account with 2FA. - All tokens launched on the memecoin platform are **client-side apptokens** requiring a signature from the user's device for token interactions. - A user has used the same password across many online accounts, and their main password is leaked in a hack of another service. - Hackers log in to their account on the memecoin platform to steal funds. - When the hackers prompt a transfer of the account's tokens, the transfer is secured by 2FA. - The hackers attempt to export the wallet’s private key in order to transfer through a separate wallet service, but the transfers are blocked by the token contract as the 2FA requirement has not been met. # Novel offerwall products generating ad revenue ### How can client-side apptokens be used to build novel offerwall products? With **client-side apptokens**, developers have the ability to direct their users to download third-party applications in order to interact with a token. In web2, **offerwall products** convert demand for in-game assets by directing users to complete tasks on behalf of third parties in order to receive in-game rewards. Unity estimates that **38% of game developer ad revenues** are from offerwalls. **Client-side apptokens** allow developers to monetize a similar strategy with their **web3 token economies**, syncing the demand for their tokens with offerwall task conversion. For example: # - Game developer is monetizing an offerwall in their web3 game. - Players have expressed enormous demand for a new resource token in the game that is used for a specific event. - This token can be acquired through gameplay and by purchasing the token from other players. - The resource token is programmed as a **client-side apptoken** which requires players to install third-party application/s in order to trade. - Demand to trade the resource token is now converted into offerwall task completion, generating the game developer additional revenue. ## Trade Apptokens **Trade apptokens** are tokens which are programmed with novel trade mechanics by the developer. These tokens can be used to invent new experiences around trading tokens or to incentivize specific user activity. ### What new opportunities do trade apptokens unlock? 1. [Define exclusive trading pairs](#exclusive-trading-pairs) 2. [Create apptokens that trade in specific price ranges](#specific-price-ranges) 3. [Create apptokens which trade in fixed ratios](#fixed-ratios) 4. [Directly monetize demand to access trading](#monetize-demand) 5. [Monetize trade volume](#monetize-volume) 6. [Distribute demand for an apptoken to other assets](#distribute-demand) 7. [Monetize and improve user engagement with lotteries](#lotteries) --- # Define exclusive trading pairs ### How can trade apptokens be used to define exclusive trading pairs? **Trade apptokens** can be implemented with parameters defining which tokens or apptokens they are eligible to trade with. This is a novel feature for web3 tokens which enables the development of more insulated economies. **Fixed-pair apptokens** are especially useful for developers operating complex digital economies that are best served disconnected from the broader crypto market. Game developers in particular are used to managing **web2 economies** within which broader macro forces have little to no bearing. Liquidity is provided between assets within the economy as opposed to flowing out of it. For example: # - Game developer is operating a web3 apptoken economy for their 4X game. - Developer wishes to insulate their token trading activity to assets within their game. - Developer sells a base ecosystem token, `$GOLD`, which is a **fixed-pair apptoken** that can only be exchanged for other resources in the game. - Resources earned and purchased in the game can be swapped into `$GOLD`, but not exchanged for tokens that are not on the developer's pair whitelist. # Create apptokens that trade in specific price ranges ### How can trade apptokens be used to create price ranges for apptoken trading? **Trade apptokens** can be implemented with parameters that define a price range in which the apptoken can trade. This can include a minimum price, a maximum price or both. Natively defined price ranges for apptoken trading are an extremely powerful new feature enabled by apptokens. Developers with complex, multi-asset digital economies may wish to express the relative value of the different assets in their economy through a price range in order to reliably balance their economy. This is extremely important as token pricing becomes deeply dependent on the utility, emissions, and pricing of related tokens in the economy. Price ranges offer stability thresholds that the developer can trust. For example: # - Game developer has 5 different apptokens representing metals in their game. In ascending order of relative value: `$BRASS`, `$IRON`, `$STEEL`, `$GOLD`, `$PLATINUM`. - The different metals are designed to be increasingly rare and critical for leveling up a player's gear. - The developer wishes to create a series of price ranges that reflects the relative value of the apptokens: - `$BRASS` trades between $0.10 - $1.00 per apptoken - `$IRON` trades between $1.00 - $3.00 per apptoken - `$STEEL` trades between $3.00 - $10.00 per apptoken - `$GOLD` trades between $10.00 - $50.00 per apptoken - `$PLATINUM` trades between $50.00 - $250.00 per apptoken # Create apptokens which trade in fixed ratios ### How can trade apptokens be used to create trading ratios between different apptokens? **Trade apptokens** can be implemented with parameters which define ratios in which specific apptokens can trade. Similar to price ranges, this is a novel token feature which enables developers to explicitly define the relative value of tokens in their digital economies. It is critical for developers operating multi-asset digital economies to have tools to balance the relative value of assets in order to provide the optimal digital experience to users. Game developers are a prime example - in web2 they exert complete control over how their digital assets relate to one another. **Trade-ratio apptokens** are yet another new implementation of tokens that enables similar balancing capabilities in web3. For example: # - Game developer has 5 different apptokens representing metals in their game. In ascending order of relative value: `$BRASS`, `$IRON`, `$STEEL`, `$GOLD`, `$PLATINUM`. - The different metals are designed to be increasingly rare and critical for leveling up a player's gear. - The developer wishes to create a series of trade ratios that reflects the relative value of the apptokens: - `$BRASS` trades for `$IRON` at a ratio of 5:1 - `$IRON` trades for `$STEEL` at a ratio of 2:1 - `$STEEL` trades for `$GOLD` at a ratio of 10:1 - `$GOLD` trades for `$PLATINUM` at a ratio of 3:1 - These ratios can be enforced across multiple pairs, for example, `$BRASS` trades for `$PLATINUM` at a ratio of 300:1 # Directly monetize demand to access trading ### How can trade apptokens be used to directly monetize demand to access trading? **Trade apptokens** can be implemented with an "entrance fee" for trading, where the user pays the developer in order to access token trading. This transforms trading into a feature of an application or game which can be monetized at unlock. **Paid trade apptokens** are especially useful if trading is a secondary use-case of a developer's token. The developer can design the apptoken to operate primarily as a spendable utility, but then monetize any excess demand for trading the apptoken by implementing the **pay-to-trade** feature. For example: # - Game developer has an apptoken, **$SILVER**, which is emitted from mining activities in game and used to upgrade gear. - The game developer wishes to encourage players to mine **$SILVER** themselves, as the engagement with the activity has a noticeable impact on player retention over time. - The game developer designs **$SILVER** as a **paid trade apptoken**, and monetizes the excess demand to trade the token while incentivizing users to earn it for themselves. # Monetize trade volume ### How can trade apptokens be used to monetize trade volume? **Trade apptokens** can be implemented with native trading fees at the token smart contract level. **Trade apptokens** offer a sophisticated implementation of trading fees or taxes which unlock a reliable revenue stream for developers when their assets are traded. **Tax apptokens** offer a dynamic solution for monetizing trade volume, developers are able to update their token tax parameters on the fly and even design segmented taxes for different user groups. For example: # - Social media application developer launches a native utility token **$LIKE** for their product. - **$LIKE** is used in the social media application to reward popular posts. Users spend **$LIKE** when they engage with posts. - **$LIKE** is generating a large amount of trading volume and is implemented as a **tax apptoken**. - **$LIKE** has a variable tax rate depending on which user is trading the token: - 25th percentile of followed accounts: 1% trading fee - 25-75th percentile of followed accounts: 0.5% trading fee - 75-99th percentile of followed accounts: 0.25% trading fee - 99th percentile of followed accounts: 0.1% trading fee - These variable tax rates reward the desired behavior of users on the platform, which grows the social network. # Distribute demand for an apptoken to other assets ### How can trade apptokens be used to distribute demand for an asset to other assets? **Trade apptokens** can be implemented with parameters that require users who wish to trade the apptoken to hold one or more other assets in order to do so. This new feature resembles a popular **NFT mint mechanic** where the NFT smart contract would check the wallet of a minter to confirm they owned a required asset in order to process the mint. This feature can now be extended natively into the token code to require specific asset ownership to enable trading of the apptoken. This is especially useful for developers who wish to distribute the demand for an apptoken among other tokens in their economy. This can be used to balance a digital economy or to incentivize users to collect and hold a set of tokens in order to continue to participate in the trading ecosystem. For example: # - A game developer is introducing a new apptoken, **$IRON**, into their game. - **$IRON** will be used to craft a new series of items which have the highest stats in the game. - There is a lot of early demand for the **$IRON** apptoken, and the developer is concerned the supply may be cornered by speculators who aren’t players which could harm the balance of their economy. - To prevent this, the developer implements **$IRON** to require that users who wish to trade the apptoken also own a set of other assets in their game. # Monetize and improve user engagement with lotteries ### How can trade apptokens be used to create native lottery dynamics to access trading? **Trade apptokens** can be implemented with native lottery mechanics at the token smart contract level. These lottery mechanics unlock fun new ways for a developer to monetize demand for their tokens and reward users with the ability to trade. **Lottery apptokens** can be designed with either raffle or sweepstakes mechanics. Raffle lottery apptokens monetize the lottery itself by selling users tickets, with winning tickets unlocking the ability to trade a token. Sweepstakes lottery apptokens are free lotteries where users complete a series of signup steps in order to get the chance to win the ability to trade a token. Furthermore, lotteries may be implemented to gate only one side of a trade. The lottery winners may be able to sell the token, but then any user could have the ability to buy the token. This is especially useful if the developer wants to disincentivize one type of trading behavior (e.g., selling) and encourage another. For example, a raffle lottery apptoken: # - Game developer has issued a resource apptoken **$LEATHER** which is used in game to craft clothing. - Clothing is a rare cosmetic in the game, and the developer wishes to focus on incentivizing players to collect **$LEATHER** by completing tasks in the game. - There is a lot of demand for trading of **$LEATHER** between players. - Developer decides to execute a raffle which rewards winners the right to trade their **$LEATHER** holdings. - Developer decides there will be 100 winners of the raffle out of 10,000 **$LEATHER** holders, so the incentive to play the game to accrue **$LEATHER** stays intact. - Developer sells raffle tickets for the lottery in an auction, efficiently monetizing the demand to trade **$LEATHER**. For example, a sweepstakes lottery apptoken: # - Developer decides to execute a sweepstakes which rewards winners the right to trade their **$LEATHER** holdings. - Developer requires sweepstakes participants to complete another in-game task which has been shown to significantly improve player retention. - Developer efficiently converts demand for trading **$LEATHER** into boosted player retention using the sweepstakes. ## Temporal Apptokens **Temporal apptokens** are tokens which are programmed with novel mechanics regarding how they relate to periods of time. These tokens can be used to improve user distributions, control token lifecycles, and implement novel seasonal activity natively into the token code. ### What new opportunities do temporal apptokens unlock? 1. [Improved token distributions with native vesting](#improved-token-distribution) 2. [Control token lifecycles with expiries](#control-token-lifecycles) 3. [Implement event-based token activity](#event-based-activity) --- # Improved token distributions with native vesting ### How can temporal apptokens be used to improve token distributions? **Temporal apptokens** can be implemented with native vesting parameters that unlock token trading over a specific period of time. This is especially useful if a large number of tokens were distributed at once to a broad audience of users, where immediate access to trading may artificially depress the token market. These **vesting** apptokens can be designed to unlock all tokens on a **'cliff'**, or gradually along a curve. For example: # - Game developer decides to airdrop a new resource token for an upcoming event to all players over level 10. - Developer is concerned that too much trading of the token after the distribution may affect the demand for the event, and decides to implement the token as a **vesting apptoken**. - The developer programs the token balance for each player to unlock 1/12 each day. - The market for the token at distribution is healthy, and the token unlocks each day are met with sufficient purchase demand from other players. # Control token lifecycles with expiries ### How can temporal apptokens be used to control token lifecycles? **Temporal apptokens** can be implemented with expiries which prevent further token trading after a specific period of time. These **'expiry'** apptokens can be used by developers to sunset tokens in their digital economies. Before **expiry apptokens**, any web3 token released by a developer is live in perpetuity. This is in contrast to web2 digital economies, where certain in-game/in-application assets can expire in their utility as time passes. For example: # - One of the first resource tokens released in a web3 game was **$FIREWOOD**, which players collected in order to build fires. - Later in the lifecycle of the game, the developer introduced **$WOOD** as a blanket resource for building structures as well as fires. - The **$FIREWOOD** token was to be replaced by the new **$WOOD** token, so the developer implemented a token expiry. - On a specific date, no more interactions with the **$FIREWOOD** token were possible. # Implement event-based token activity ### How can temporal apptokens be used to implement seasonal token activity? **Temporal apptokens** can be implemented with parameters that make token trading available during specific windows. These **'event' apptokens** can be used by developers to design novel event-based token systems. With **event apptokens**, it is now possible to coordinate demand for a web3 game or application token around a specific time-based event. In web2 digital economies, assets are often used in coordination with events to create exciting experiences for users. For example: # - It is the holiday season, and a game developer decides to do a Christmas event where players can earn and trade a new resource token **$SNOW**. - **$SNOW** can be spent in-game to unlock new levels and items. - The developer wishes to only make **$SNOW** available for trading and spending during the Christmas season each year, and designs the token as an **event apptoken**. - During the window of **December 1st - December 31st** each year, the **$SNOW** token is available for trading and spending. When the period ends, the token becomes inert. ## Spend-only Apptokens **Spend-only apptokens** are a novel token type enabled by **apptokens** which are designed to be specifically spendable inside a developer’s ecosystem. There are many new features enabled by tokens which are designed to be spent, including: promotional tokens for user acquisition, pure resource tokens for games, and pure utility tokens for applications. ### What new opportunities do spend-only apptokens unlock? 1. [Improved user acquisition with promotional tokens](#improved-user-acquisition) 2. [Pure resource tokens for games](#pure-resource-tokens) 3. [Pure utility tokens for applications](#pure-utility-tokens) --- # Improved user acquisition with promotional tokens ### How can spend-only apptokens be used to improve user acquisition? **Promotional spend-only apptokens** are a novel and extremely cost-efficient method to execute **web3 native user acquisition**. These tokens are implemented to be used for a specific spend in a digital economy, and are then distributed by a developer as a means of acquiring users for their game and application. By distributing a **promotional spend-only apptoken**, a developer can give away something that new users value but that doesn't necessarily impact their broader asset economy. This is especially useful for developers with complex economies consisting of multiple apptoken types with different features. For example: # - Game developer has noticed that the most popular new user activity in their game is **character appearance customization**. - Developer designs a **promotional spend-only apptoken** which can be used to unlock new character skins. - Developer launches a campaign where existing players can generate referral links, and for every friend they sign up to the game they both receive the **promotional spend-only apptoken**. # Pure resource tokens for games ### How can spend-only apptokens be used to design pure resource tokens for games? **Spend-only apptokens** can be implemented to be linked to a specific spend or set of spends in a video game. This unlocks a new token type for web3 developers which is closer to how many resources in web2 video games operate. In a complex, multi-asset digital economy, not all assets need to be or should be tradable between players. The developer may wish to make certain assets **spend-only** as their data indicates that the earning and spending of that asset improves important metrics such as player retention. For example: # - Game developer notices that players who upgrade their initial gear from leather to wood are 80% more likely to return to play the game the next day. - Developer decides to implement the **$LEATHER** and **$WOOD** tokens as **spend-only resource apptokens** in order to encourage players who wish to upgrade their gear to earn the resources for themselves and spend them on their intended function. --- # Pure utility tokens for applications ### How can spend-only apptokens be used to design pure utility tokens for applications? **Spend-only apptokens** can be implemented to be linked to a specific spend or set of spends in an application. Application developers who wish to tightly bind tokens to utility and spends within their digital experience can now do so with **apptokens**. **Spend-only utility apptokens** are a powerful tool to focus user activity on critical feedback loops in the product. Before **spend-only apptokens** it was extremely difficult to create tight token:utility feedback loops due to the impact of open market trading. Now, the token is implemented with its intended utility natively in the smart contract. For example: # - Messaging app has a crypto economy natively built into the messaging experience. - Developer notices that users are much more likely to continue to use the messaging app if they purchase 5 sticker packs with the apptoken **$STICKER**. - Developer implements **$STICKER** as a **spend-only utility apptoken** which can be purchased from the application store or received as a promotional apptoken and then exclusively spent on sticker packs. --- Compliant Apptokens **Compliant apptokens** are a novel token type which are implemented by a developer with features that reflect a given compliance regime. This is an enormous unlock for developers looking to issue regulated assets on-chain, as a **compliant apptoken** can explicitly define and then comply with any theoretical compliance policy that a given asset type or regulatory regime demands. ### What new opportunities do compliant apptokens unlock? 1. [Comply with KYC requirements for asset ownership](#comply-with-kyc) 2. [Create complex permissions for asset ecosystem interactions](#create-complex-permissions) --- Comply with KYC requirements for asset ownership ### How can compliant apptokens be used to fulfill KYC requirements for asset ownership? Many assets in capital markets - particular real-world assets (RWAs) - have strict **KYC** requirements for ownership and trading. Before apptokens it was exceedingly difficult for blockchain-based digital assets to reflect **KYC** requirements due to limited features in the token smart contract. With apptokens, **KYC requirements** for any given asset in any given regime can be implemented natively into the token smart contract, enabling the compliant issuance and trading of regulated **RWAs** on-chain for the first time. For example: # - An equities broker notices that many of their clients have large crypto portfolios. - Broker decides to offer **tokenized securities** as a purchasable asset to its clients in order to tap their crypto liquidity. - Broker implements **tokenized securities** as **KYC apptokens** with native KYC-checks embedded into the smart contract at point of trade. # Create complex permissions for asset ecosystem interactions ### How can compliant apptokens be used to create complex permissions for asset ecosystem interactions? **Apptokens** can be used to synchronize compliance across any number of regulatory regimes simultaneously. For cases where assets have holders across multiple national/local jurisdictions, which each have unique compliance policies for a given asset type, a robust and extensive permissioning standard is required at the token smart contract level. For example: - An American business issues a **digital security**. - This digital security requires a complex, multi-jurisdiction compliance framework to be traded: - In the USA, only **accredited investors** can own the digital security. - In the EU, the asset must be **registered under MiCA**. - The asset must **not be traded by any individuals** from **OFAC** sanctioned jurisdictions. - etc. - The issuer of the digital security implements the **apptoken** as a **multi-KYC apptoken** to conform to all of these regulatory requirements simultaneously. ## Backed Apptokens **Backed apptokens** are a novel token type enabled by the combined utilization of the **ERC20-C apptokens standard** with the **TokenMaster protocol**. **Backed apptokens** are bought and sold through **TokenMaster pools** which store backed value. **Backed apptokens** can be reliably exchanged for their underlying value via the **TokenMaster pool** that launched the apptoken. ### What new opportunities do backed apptokens unlock? 1. [Launch tokens with price stability](#launch-tokens-with-price-stability) 2. [Monetize spends of backed apptokens](#monetize-spends) --- # Launch tokens with price stability ### How can backed apptokens be used to launch tokens with price stability? Utilizing the **TokenMaster protocol**, developers can launch **backed apptokens** in pools paired with other **ERC20 tokens** or **ERC20-C apptokens**. When the backed apptoken is purchased in exchange for the paired token, the paired token is deposited into the **TokenMaster pool** and the backed apptoken is minted. When the backed apptoken is sold back into the pool, the paired token is withdrawn and the backed apptoken is burnt. This dynamic allows developers to configure tokens with much more price stability than traditional ERC20 tokens. Developers can disable external **AMM** trading of the issued token. Developers can also configure fees that monetize trading activity in and out of **TokenMaster pools**. For example: - Developer wishes to issue an apptoken, **BKD**, with a fundamental value equivalent to **0.01 ETH**. - Developer launches a **TokenMaster pool** where **0.01 ETH** can be exchanged for **1 $BKD**. - Developer disables trading on **external AMMs**. - Developer collects a **1% fee** on every trade through the **TokenMaster pool**. - Now users who purchase **$BKD** receive a **backed apptoken** which can be exchanged for the underlying **ETH** at any time. Monetize spends of backed apptokens ### How can backed apptokens be used to monetize spends? **Backed apptokens** can be implemented with spends that burn the backed apptoken and transfer the backing to the developer. This is a critical piece of apptoken infrastructure for developers looking to generate direct revenue from their apptokens ecosystem. Now, developers can issue true value-backed apptokens and then monetize those tokens by connecting them directly to spends in their digital economies. For example: - **BKD** is a **backed apptoken** that is backed by **0.01 ETH** per **BKD**. - Developer creates an in-game spend where **BKD** can be exchanged for desirable cosmetics. - Users spend their **BKD** on cosmetics, and the underlying **ETH** is transferred to the developer. **Creator Token Standards** offers creator-defined guardrails to the transfer function of ERC20, ERC721, and ERC1155 tokens. In its simplest form, this allows creators to decide what protocols can be used to transfer NFTs and other coins, whether it be for trading, on-chain games, or other custom use cases involving the transfer of tokens. All common token protocols (ERC20, ERC721, and ERC1155) include some form of transfer function. The typical base implementation of each standard includes a before transfer hook and an after transfer hook. Creator tokens share a base contract that ties into these hooks and adds guardrails using an external transfer validation registry to apply a creator-defined set of rules allowing or blocking transfers based upon the caller, from, and to address. This allows creators to dictate what protocols may interact with token transfers for their own projects. Collection creators interact with the transfer validation registry to configure the ruleset for transfers, and they are free to change their ruleset at any time to escalate or de-escalate the security of their collections. Creators apply two settings to their collections: (1) the transfer security level to apply to the collection and (2) the list id to apply to the collection. To deploy a collection without writing code, or to update settings visit [developers.freenft.com](https://developers.freenft.com). # Creator Token Standards v5 # Overview ## 5.0.0 (2025-05-05) ### New Features Version 5 of the Transfer Validator is focused on making improvements that keep up with changes on the Ethereum network to preserve the wallet to wallet OTC transfers in most cases and to add new features enabled by EIP-7702. In Version 5, Limit Break also goes beyond the scope of EIP-7702 and has future-proofed the Transfer Validator so that future updates can be made in order to keep up with additional changes to Ethereum that haven't even been considered or proposed yet. The re-design of the Transfer Validator will allow Limit Break to continue to adapt to future EIPs that could impact 20-C/721-C/1155-C without requiring migrations or actions from creators to keep up-to-date. The following subsections detail the key changes made from Version 4 to Version 5. - [Replaced Transfer Security Levels With Modular Rulesets](#replaced-transfer-security-levels-with-modular-rulesets) - [Feature: Ruleset Registration](#feature-ruleset-registration) - [Feature: Ruleset Binding](#feature-ruleset-binding) - [Feature: Automatic Updates](#feature-automatic-updates) - [Feature: Enhanced AMM Compatibility For Apptokens](#feature-enhanced-amm-compatibility-for-apptokens) - [Feature: Default List Extension](#feature-default-list-extension) - [Feature: EIP-7702 OTC Option](#feature-eip-7702-otc-option) - [Feature: Smart Wallet OTC Option](#feature-smart-wallet-otc-option) - [Feature: Global and Ruleset Options](#feature-global-and-ruleset-options) - [Feature: Whitelist Extension Contracts](#feature-whitelist-extension-contracts) - [Feature: Expansion Lists](#feature-expansion-lists) - [Feature: Expansion Settings](#feature-expansion-settings) - [Feature: Transfer Validation Simulation Functions](#feature-transfer-validation-simulation-functions) ### Replaced Transfer Security Levels With Modular Rulesets Prior versions of the transfer validator combined all the logic for security levels into a large, monolithic validation function. Version 5 replaces this monolithic function and transfer security levels with the concept of `Rulesets`. `Rulesets` are entirely read-only and unable to change EVM state, as their sole purpose is to determine if a given transfer should be allowed or blocked. Version 5 will initially ship with four rulesets that replicate and improve upon the previous nine transfer security levels (Vanilla, Soulbound, Blacklist, and Whitelist Rulesets). #### Feature: Ruleset Registration Over time, Limit Break can register new ruleset modules. These modules could be entirely new types of validation rulesets, or they can be upgrades to existing rulesets such as an augmented Whitelist ruleset that offers new transfer validation options, or patches issues created by new EIPs. Because ruleset modules must not be able to modify EVM state during execution, the module registration process includes a code purity check. Every opcode in the module is checked against a set of invalid or banned opcodes that have the potential to change state, ensuring that registering a module that modifies the EVM at validation time is impossible. #### Feature: Ruleset Binding Upon successful registration, rulesets are bound to `Ruleset Ids`. Usable Ids range from 0-254. Ruleset Id 0 is reserved for the default ruleset of the validator. It is currently bound to Ruleset Whitelist, so unless otherwise modified by the creator, the default validation ruleset for a 20-C/721-C/1155-C token is Whitelist with all default global and ruleset-specific option flags set to `0`. Ruleset Id 255 is reserved for creators that wish to opt out of Automatic Updates (more on this in a moment). With up to 255 ruleset id bindings available, there is a lot of room for future expansion of the Creator Token Transfer Validator capabilities. #### Feature: Automatic Updates The system of registration and binding rulesets creates the opportunity for creators to get passive security upgrades to their creator tokens. In prior versions of the validator, the creator would have to migrate to a new validator. Once migrated to Version 5, creators automatically get access to enhancements without having to be notified to take specific actions! ***Note: Creators can opt out of the Automatic Update feature by using Ruleset Id 255 for their token security policies and specifying a specific registered ruleset they want to use. When creators specify the exact ruleset address they wish to use this way, rebindings of rulesets to other ids will not change the validation behavior of their tokens.*** ### Feature: Enhanced AMM Compatibility For Apptokens Validator V5 enhances the pre-existing authorizer mode for token transfers by implementing support for authorized token amounts, extending support from ERC721-C to ERC20-C and ERC1155-C apptokens. This improved authorization mode can be used in Uniswap V4 hooks to create powerful apptokens with rules that execute inside of Uniswap! When the hook’s rules for a token have been satisfied, the hook can inform Validator V5 that the AMM address is authorized to transfer X number of apptokens. In the coming weeks, Limit Break will release a production-grade Apptoken Hook for Uniswap V4. Furthermore, Limit Break is nearing completion of a native TokenMaster AMM for ERC20-C apptokens. TokenMaster AMM will be the best protocol for developers to program the trading rules for their tokens. For developers, this means they can apply rules to AMMs that include, but are not limited to: - LP Whitelisting - Token pairing - Custom token pricing ranges - Fixed price tokens - Swap fees - Trading caveats such as NFT-gated swaps or task-gated swaps ### Feature: Default List Extension In prior versions of Transfer Validator, when creators switched to a custom list id instead of the default, they had to fully manage the custom list. If they still wanted access to default whitelisted protocols, for example, the creator had to add those protocols to their custom list in addition to the extra protocols they put into the list. Updates to the default list were not automatically reflected in the creators' custom lists either. Version 5 adds an option to extend the default list with a custom list, making the addition of custom protocols much easier. When the creator opts into supplementing their custom list with the default list, they receive automatic updates that follow the custom list, plus they can easily add the additional protocols they want to work with to their custom list and both lists will be checked during transfer validation. ### Feature: EIP-7702 OTC Option The Whitelist ruleset includes a new option to either allow or block OTC transfers when the OTC transfer is performed by an EOA with an EIP-7702 delegation attached. This option is only evaluated when a collection is not configured to block all OTC. Because of the risk that transfer rules are bypassed by EOAs with 7702 delegates attached, the default behavior is to Block OTC from EOAs in delegation mode. Creators can toggle this setting to opt out of this protection and allow OTC from EOAs in delegation mode. Additionally, when blocking EOAs with delegates creators can whitelist trusted delegate code addresses, codehashes, or even specific delegate code factories that are known to be safe to broaden access to OTC transfers to as many users as possible. The ability to whitelist delegates is a powerful new features that sets the stage for the development and proliferation of appwallets, which are 7702 delegate implementations built to work with apptokens. ### Feature: Smart Wallet OTC Option As account abstraction wallets gain popularity, more and more users will hold tokens in smart contract wallets. The Whitelist ruleset includes a new option to either allow or block OTC transfers when the OTC transfer is performed by a smart wallet. This option is only evaluated when a collection is not configured to block all OTC. The default behavior is to Block OTC from Smart Wallets. Creators can toggle this setting to opt out of this protection. Additionally, when blocking OTC from smart wallets, creators can whitelist trusted smart wallet code addresses, codehashes, or even specific smart wallet factories that are known to be safe to broaden access to OTC transfers to as many users as possible. ### Feature: Global and Ruleset Options The Token Security Policy now includes an 8-bit `Global Options` field and a 16-bit `Ruleset-Specific Options` field. Global options generally apply to many rulesets (for example: account freezing and authorizer mode flags). Ruleset-specific options generally apply to a single ruleset. For example, the Whitelist ruleset makes use of 5 flags that fine-tune Whitelisting ruleset behavior. New ruleset implementations can make use of up to 16 ruleset-specific option flags in addition to the standard global options. ### Feature: Whitelist Extension Contracts Version 5 introduces a new way to make more complex decisions about what accounts should be whitelisted, going beyond account address and codehash. Creators may add external contracts that query wallet factories, for example. This expands the possibilities for easily whitelisting protocols that create many contract instances, such as a wallet factory or AMM with individual pools. ### Feature: Expansion Lists Over time, new rulesets may require the use of new kinds of lists. Prior versions of the transfer validator only included blacklists, whitelists, and authorizer lists. The creation of a system of expansion lists allows new rulesets to incorporate new list types that can be attached to any list id. For example, the following list types are currently defined: | List Type | List Type Id | Used In Rulesets | |-------------------------------------------------|--------------|-----------------------| | Blacklist | 0 | Blacklist | | Whitelist | 1 | Whitelist | | Authorizers | 2 | Blacklist + Whitelist | | Whitelist Extension Contracts | 3 | Whitelist | | EIP-7702 Delegate Whitelist | 4 | Whitelist | | EIP-7702 Delegate Whitelist Extension Contracts | 5 | Whitelist | Version 5 of the Transfer Validator supports up to 256 total list types, plenty of room for future expansion. ### Feature: Expansion Settings Over time, new rulesets may require the use of new token-specific settings. Version 5 includes a system for setting arbitrary expansion settings as key-value pairs. Key-value pairs come in two forms: Expansion Words and Expansion Datums. Expansion Words have 32-byte keys and 32-byte values. Expansion Datums have 32-byte keys and variable length bytes values for maximum flexibility and future expansion. At this time, no rulesets currently use expansion settings, and this feature is solely for future-proofing. ### Feature: Transfer Validation Simulation Functions Simulation functions have been added as a convenience that make it easier for D'apps to check whether or not a transfer operation would be successful based upon configured rulesets, options, and whitelists. These functions are: ```solidity /** * @notice Simulates transfer validation of a collection with its current ruleset and options. * * @dev Authorization mode overrides are not taken into account. * * @param collection The address of the collection. * @param caller The address initiating the transfer. * @param from The address of the token owner. * @param to The address of the token receiver. * @return isTransferAllowed True if the transfer is allowed, false otherwise. * @return errorCode The error code if the transfer is not allowed. */ function validateTransferSim( address collection, address caller, address from, address to ) external view returns (bool isTransferAllowed, bytes4 errorCode); /** * @notice Simulates transfer validation of a collection with its current ruleset and options. * * @dev Authorization mode overrides are not taken into account. * * @param collection The address of the collection. * @param caller The address initiating the transfer. * @param from The address of the token owner. * @param to The address of the token receiver. * @param tokenId The token id being transferred. * @return isTransferAllowed True if the transfer is allowed, false otherwise. * @return errorCode The error code if the transfer is not allowed. */ function validateTransferSim( address collection, address caller, address from, address to, uint256 tokenId ) external view returns (bool isTransferAllowed, bytes4 errorCode); /** * @notice Simulates transfer validation of a collection with its current ruleset and options. * * @dev Authorization mode overrides are not taken into account. * * @param collection The address of the collection. * @param caller The address initiating the transfer. * @param from The address of the token owner. * @param to The address of the token receiver. * @param tokenId The token id being transferred. * @param amount The amount of the token being transferred. * @return isTransferAllowed True if the transfer is allowed, false otherwise. * @return errorCode The error code if the transfer is not allowed. */ function validateTransferSim( address collection, address caller, address from, address to, uint256 tokenId, uint256 amount ) external view returns (bool isTransferAllowed, bytes4 errorCode); ``` # Contract Deployments Creator Token Transfer Validator v5.0.0 can be permissionlessly deployed by any wallet to the same addresses on any EVM compatible chain. #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Creator Token Transfer Validator V5.0.0 | `0x721C008fdff27BF06E7E123956E2Fe03B63342e3` | | EOA Registry | `0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C` | | Module Collection And List Management | `0x721C002d2CAe3522602b93a0c48e11dC573A15E3` | | Module Ruleset Bindings | `0x721C0086CC4f335407cC84a38cE7dcb1560476B0` | | Ruleset Whitelist | `0x00721CE73fDE1e57A9048E0e19B7ee7c8D3F10E3` | | Ruleset Blacklist | `0x00721C2b7f77bBC59659E65BE4bb799D94995844` | | Ruleset Vanilla | `0x00721CC6Ea8D83309720e2b5581529d2B3762b73` | | Ruleset Soulbound | `0x00721Cbc023e08a3DAf2d21b683C7F0CB281cccf` | ***Note: If Creator Token Transfer Validator is not yet deployed on your chain, use [the infrastructure deployment tool at developers.apptokens.com/infrastructure.](https://developers.apptokens.com/infrastructure)*** # Basic Usage Most creators will find that the default settings meet their needs. Following the default settings on the Version 5 Transfer Validator offers creators the following: - Default Lists (List Id 0) - Whitelist Apptoken Protocols - Payment Processor - TokenMaster - Authorizer List - Seaport Trading with OpenSea Royalty Sign-Off - Seaport Trading with Reservoir Royalty Sign-Off - Trusted Smart Wallets and EIP-7702 Delegates List - Default Ruleset - Whitelist with following options - Authorization Mode: Enabled - Account Freezing Mode: Disabled - Default List Extension: N/A - Allow OTC: Enabled - Allow EIP-7702 Delegate OTC: Disabled - Allow Smart Wallet OTC: Disabled - Block Smart Wallet Receivers: Disabled - Block Unverified EOA Receivers: Disabled - Automatic Updates: Default ruleset may be patched and upgraded by Limit Break as EVM evolves to keep creators protected. Default whitelist and authorizer list kept up to date as new protocols are released. To configure tokens to use default settings, update to the latest [Creator Token Standards](https://github.com/limitbreakinc/creator-token-standards) library if using Foundry or the [NPM Creator Token Standards](https://www.npmjs.com/package/@limitbreak/creator-token-standards) library if using Hardhat, and deploy a new ERC20-C/ERC721-C/ERC1155-C token. For collections that are already live, change the validator address to Version 5 Transfer Validator at any time by calling the following function on your collection. ```solidity function setTransferValidator(address validator) external; ``` Transfer Validator addresses are documented at [apptokens.com](https://apptokens.com/docs/category/creator-token-standards), and Limit Break provides [Developer Tools to configure your validator here](https://developers.apptokens.com/modification). # Advanced Usage (Going Beyond The Defaults) The Transfer Validator gives creators complete control over their token ecosystem. This section explores the customization options available to creators. #### 1. Select A Ruleset Transfer Validator Version 5 currently includes four ruleset modules. | Ruleset | Id Binding | Global Options | Ruleset Options | Default Ruleset | Description | |-----------|------------|----------------|------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Whitelist | 0, 4 | GO0/1/2/3 | WLO0/1/2/3/4/5 | ✔ | Encompasses Security Levels 3 through 8 from prior validators. Blocks all transfers, unless allowed through an authorizer, a whitelist, or other whitelisting option. | | Vanilla | 1 | None | None | | Equivalent to Security Level 1 from prior validators. Allows all transfers. | | Soulbound | 2 | None | None | | Equivalent to Security Level 9 from prior validators. Blocks all transfers. | | Blacklist | 3 | GO0/1/2/3 | None | | Equivalent to Security Level 2 from prior validators. Allows all transfers, unless the operator (msg.sender) is explicitly blocked by address or codehash. | #### 2. Set Global and Ruleset-Specific Options ***Note: By default, all options bits are set to zero (off).*** **Global Options Table** | Option Code | Option Name | Bit | Rulesets | |-------------|-----------------------------------------------------|-----|---------------------------| | GO0 | Global Option Disable Authorization Mode | 0 | Whitelist
Blacklist | | GO1 | Global Option No Authorizer Wildcard Operators Mode | 1 | Whitelist
Blacklist | | GO2 | Global Option Account Freezing Mode | 2 | Whitelist
Blacklist | | GO3 | Global Option Default List Extension Mode | 3 | Whitelist
Blacklist | | GO4 | Reserved | 4 | | | GO5 | Reserved | 5 | | | GO6 | Reserved | 6 | | | GO7 | Reserved | 7 | | **Ruleset Whitelist Options Table** | Option Code | Option Name | Bit | Rulesets | |-------------|-----------------------------------------------------|-----|-----------| | WLO0 | Whitelist Option Block All OTC | 0 | Whitelist | | WLO1 | Whitelist Option Allow OTC For 7702 Delegates | 1 | Whitelist | | WLO2 | Whitelist Option Allow OTC For Smart Wallets | 2 | Whitelist | | WLO3 | Whitelist Option Block Smart Wallet Receivers | 3 | Whitelist | | WLO4 | Whitelist Option Block Unverified EOA Receivers | 4 | Whitelist | | WLO5 | Reserved | 5 | | | WLO6 | Reserved | 6 | | | WLO7 | Reserved | 7 | | | WLO8 | Reserved | 8 | | | WLO9 | Reserved | 9 | | | WLO10 | Reserved | 10 | | | WLO11 | Reserved | 11 | | | WLO12 | Reserved | 12 | | | WLO13 | Reserved | 13 | | | WLO14 | Reserved | 14 | | | WLO15 | Reserved | 15 | | #### 3. Choose A List, Or Default List Extension Most rulesets use lists of accounts or codehashes to allow or deny transfers. These lists function much like a firewall with an access control list. This on-chain access control list governs what operators are allowed, etc. Lists are identified and accessed by a key derived from two pieces of information. First, the List Id, is a uint48 value ranging from 0 (Default List Id Managed By Limit Break) to 281,474,976,710,655. Second, the List Type, is a uint8 value ranging from 0 to 255 that partitions the List Id into multiple kinds of lists. **List Ids** | List Id | List Manager | Description | |-------------------------|----------------------------------------------------------------------|--------------------------| | 0 | Limit Break | Default List | | 1 - 281,474,976,710,655 | Developers / Creators
Exchanges
DAOs / Enterprises
| Custom / Community Lists | **List Types** | Type Id | List Type | Applicable Ruleset(s) | |---------|-------------------------------------------------|---------------------------| | 0 | Blacklist | Blacklist | | 1 | Whitelist | Whitelist | | 2 | Authorizers | Whitelist
Blacklist | | 3 | Whitelist Extension Contracts | Whitelist | | 4 | EIP-7702 Delegate Whitelist | Whitelist | | 5 | EIP-7702 Delegate Whitelist Extension Contracts | Whitelist | | 6-255 | Reserved for Future Validator Expansion | TBD | ##### Blacklists Blacklists, applicable only to the Blacklist ruleset, contain a set of account addresses and codehashes. The blacklist ruleset allows all transfers unless the operator (msg.sender) address is explicitly specified in the blacklist or the codehash of the operator is explicitly specified in the blacklist. Blacklists may extend the default blacklist, in which case the collection owner can specify a custom list id to be combined with the contents of the default blacklist with list id 0. ##### Whitelists, Whitelist Extension Contracts, and EIP-7702 Delegate Whitelists Whitelists, applicable only to the Whitelist ruleset, contain a set of account addresses and codehashes that are explicitly allowed to be the operator on transfers, or explicit accounts that can perform OTC transfers in all circumstances, or carveouts to smart contract receiver constraints. A whitelist extension contract is an external contract that can be queried to perform a check more advanced than a simple account or codehash comparison. For example, lets say that a whitelist should include any smart wallet contract that originated from a specific trusted wallet factory. An extension contract that checks to see if an account address was created by the trusted factory could be deployed and added to the whitelist extension contracts list. The following table shows how various options block transfers and how whitelists can be applied to allow the transfer in a controlled manner. | Option | Scenario | Whitelisting Checks | |--------|------------------------------------------------------------------------------------------------|---------------------| | WLO0 | When Block All OTC is enabled, and `caller` equals `from` (token owner calls transfer directly)... | Check if `caller` (owner/sender) is a whitelisted address. If not, check if codehash of `caller` is whitelisted. If not, iterate over whitelist extensions and check if `caller` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | | WLO0 | When Block All OTC is enabled, and `caller` does not equal `from` (operator/protocol-initiated transfer)... | Check if `caller` or `from` (operator/protocol/sender) is a whitelisted address. If not, check if codehash of `caller` or `from` is whitelisted. If not, iterate over whitelist extensions and check if `caller` or `from` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | | WLO0 | When Block All OTC is disabled, and `caller` does not equal `from` (operator/protocol-initiated transfer)... | Check if `caller` (operator/protocol) is a whitelisted address. If not, check if codehash of `caller` is whitelisted. If not, iterate over whitelist extensions and check if `caller` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | | WLO3 | When Block Smart Wallet Receivers is enabled, and the codelength of `to` is greater than zero... | Check if `to` (receiver) is a whitelisted address. If not, check if codehash of `to` is whitelisted. If not, iterate over whitelist extensions and check if `to` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | | WLO4 | When Block Unverified EOA Receivers is enabled, and the `to` account has not verified a signature with the EOA registry... | Check if `to` (receiver) is a whitelisted address. If not, check if codehash of `to` is whitelisted. If not, iterate over whitelist extensions and check if `to` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | | WLO1 | When Block All OTC is disabled and Allow OTC For 7702 Delegates is disabled, and `caller` equals `from` (token owner calls transfer directly), and `caller` has an EIP-7702 delegation attached... | Get the address of the delegation attached to `caller`. Check if `delegation` is a whitelisted address in the delegate whitelist. If not, check if codehash of `delegation` is whitelisted in the delegate whitelist. If not, iterate over delegate whitelist extensions and check if `delegation` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable delegate whitelist checks fail, block the transfer. | | WLO2 | When Block All OTC is disabled and Allow OTC For Smart Wallets is disabled, and `caller` equals `from` (token owner calls transfer directly), and the `caller` has code, but is not an EIP-7702 delegate... | Check if `caller` (owner/sender) is a whitelisted address. If not, check if codehash of `caller` is whitelisted. If not, iterate over whitelist extensions and check if `caller` is whitelisted by any extension in the list. When options include default list extension mode, repeat checks for the default list as well. If all applicable whitelist checks fail, block the transfer. | ##### Authorizers Authorizers are trusted smart contracts that have special permissions to override blacklist and whitelist restrictions. Special care must be taken to only add well-trusted authorizers. At the current time, the Seaport Royalty Enforcing Zone is the only contract that implements the required interfaces for an authorizer contract. The Seaport Royalty Enforcing Zone relies on an oracle to examine the royalties included in Seaport orders and sign a message authenticating that royalties are properly included in the order. The Royalty Enforcing Zone verifies the oracles signature, and once authenticated the zone uses authorization functions on the Transfer Validator to temporarily override blacklist or whitelist restrictions to allow Seaport trades that properly include full creator royalties. At the current time, OpenSea and Reservoir operate the two default trusted authorizer zones/oracles. #### 4. Choose Automatic or Manual Updates By default, Transfer Validator Version 5 allows Limit Break to upgrade and/or patch rulesets. This allows creators to have improved rulesets automatically applied to their collections without any action on their part. Some creators may prefer not to receive automatic updates. In this case, the creator can specify an exact version of registered ruleset to apply to their collections. If/when Limit Break updates ruleset bindings, collections that have specified an exact version of ruleset will not receive the update without manual action on their part. #### 5. Apply Desired Settings from Steps 1-4 Limit Break provides a convenient [Developer Tools UI to configure validator settings](https://developers.apptokens.com/modification). However, developers can apply desired settings by interacting directly with the contract on Etherscan, My Ether Wallet, Gnosis Safe, etc by calling the following list and collection management functions. **Collection-Specific Function Calls** ```solidity /** * @notice Set the ruleset id, global / ruleset options, fixed / custom ruleset for a collection. * * @dev Throws when the caller is neither collection contract, nor the owner or admin of the specified collection. * @dev Throws when setting a custom ruleset to an unregistered ruleset address. * @dev Throws when setting a ruleset id that is not bound to a ruleset address. * @dev Throws when setting a custom ruleset with a managed ruleset id. * * @dev

Postconditions:

* 1. The ruleset of the specified collection is set to the new value. * 2. Global options and ruleset-specific options of the specified collection are set to the new value. * 3. A `SetCollectionRuleset` event is emitted. * 4. A `SetCollectionSecurityPolicyOptions` event is emitted. * * @param collection The address of the collection. * @param rulesetId The new ruleset id to apply. * @param customRuleset The address of the custom ruleset to apply. Must be address(0) unless ruleset * id is RULESET_ID_FIXED_OR_CUSTOM (255). * @param globalOptions The global options to apply. * @param rulesetOptions The ruleset-specific options to apply. */ function setRulesetOfCollection(address collection, uint8 rulesetId, address customRuleset, uint8 globalOptions, uint16 rulesetOptions) external; /** * @notice Applies the specified list to a collection. * * @dev Throws when the caller is neither collection contract, nor the owner or admin of the specified collection. * @dev Throws when the specified list id does not exist. * * @dev

Postconditions:

* 1. The list of the specified collection is set to the new value. * 2. An `AppliedListToCollection` event is emitted. * * @param collection The address of the collection. * @param id The id of the operator whitelist. */ function applyListToCollection(address collection, uint48 id) external; ``` **Creating and Managing Custom Lists** ```solidity /** * @notice Creates a new list id. The list id is a handle to allow editing of blacklisted and whitelisted accounts * and codehashes. * * @dev

Postconditions:

* 1. A new list with the specified name is created. * 2. The caller is set as the owner of the new list. * 3. A `CreatedList` event is emitted. * 4. A `ReassignedListOwnership` event is emitted. * * @param name The name of the new list. * @return id The id of the new list. */ function createList(string calldata name) external returns (uint48 id); /** * @notice Creates a new list id, and copies all blacklisted and whitelisted accounts and codehashes from the * specified source list. * * @dev

Postconditions:

* 1. A new list with the specified name is created. * 2. The caller is set as the owner of the new list. * 3. A `CreatedList` event is emitted. * 4. A `ReassignedListOwnership` event is emitted. * 5. All blacklisted and whitelisted accounts and codehashes from the specified source list are copied * to the new list. * 6. An `AddedAccountToList` event is emitted for each blacklisted and whitelisted account copied. * 7. An `AddedCodeHashToList` event is emitted for each blacklisted and whitelisted codehash copied. * * @param name The name of the new list. * @param sourceListId The id of the source list to copy from. * @return id The id of the new list. */ function createListCopy(string calldata name, uint48 sourceListId) external returns (uint48 id); /** * @notice Creates a new list id, and copies all accounts and codehashes from the * specified source list for each specified list type. * * @dev

Postconditions:

* 1. A new list with the specified name is created. * 2. The caller is set as the owner of the new list. * 3. A `CreatedList` event is emitted. * 4. A `ReassignedListOwnership` event is emitted. * 5. All accounts and codehashes from the specified source list / list types are copied * to the new list. * 6. An `AddedAccountToList` event is emitted for each account copied. * 7. An `AddedCodeHashToList` event is emitted for each codehash copied. * * @param name The name of the new list. * @param sourceListId The id of the source list to copy from. * @param listTypes The list types to copy from the source list. * @return id The id of the new list. */ function createListCopy(string calldata name, uint48 sourceListId, uint8[] calldata listTypes) external returns (uint48 id); /** * @notice Transfer ownership of a list to a new owner. * * @dev Throws when the new owner is the zero address. * @dev Throws when the caller does not own the specified list. * * @dev

Postconditions:

* 1. The list ownership is transferred to the new owner. * 2. A `ReassignedListOwnership` event is emitted. * * @param id The id of the list. * @param newOwner The address of the new owner. */ function reassignOwnershipOfList(uint48 id, address newOwner) external; /** * @notice Renounce the ownership of a list, rendering the list immutable. * * @dev Throws when the caller does not own the specified list. * @dev Throws when list id is zero (default list). * * @dev

Postconditions:

* 1. The ownership of the specified list is renounced. * 2. A `ReassignedListOwnership` event is emitted. * * @param id The id of the list. */ function renounceOwnershipOfList(uint48 id) external; /** * @notice Adds one or more accounts to a list of specified list type. * * @dev Throws when the caller does not own the specified list. * @dev Throws when the accounts array is empty. * * @dev

Postconditions:

* 1. Accounts not previously in the list are added. * 2. An `AddedAccountToList` event is emitted for each account that is newly added to the list. * * @param id The id of the list. * @param listType The type of the list. * @param accounts The addresses of the accounts to add. */ function addAccountsToList(uint48 id, uint8 listType, address[] calldata accounts) external; /** * @notice Removes one or more accounts from a list of the specified list type. * * @dev Throws when the caller does not own the specified list. * @dev Throws when the accounts array is empty. * * @dev

Postconditions:

* 1. Accounts previously in the list are removed. * 2. A `RemovedAccountFromList` event is emitted for each account that is removed from the list. * * @param id The id of the list. * @param listType The type of the list. * @param accounts The addresses of the accounts to remove. */ function removeAccountsFromList(uint48 id, uint8 listType, address[] calldata accounts) external; /** * @notice Adds one or more codehashes to a list of specified list type. * * @dev Throws when the caller does not own the specified list. * @dev Throws when the codehashes array is empty. * @dev Throws when a codehash is zero. * * @dev

Postconditions:

* 1. Codehashes not previously in the list are added. * 2. An `AddedCodeHashToList` event is emitted for each codehash that is newly added to the list. * * @param id The id of the list. * @param listType The type of the list. * @param codehashes The codehashes to add. */ function addCodeHashesToList(uint48 id, uint8 listType, bytes32[] calldata codehashes) external; /** * @notice Removes one or more codehashes from a list of the specified list type. * * @dev Throws when the caller does not own the specified list. * @dev Throws when the codehashes array is empty. * * @dev

Postconditions:

* 1. Codehashes previously in the list are removed. * 2. A `RemovedCodeHashFromList` event is emitted for each codehash that is removed from the list. * * @param id The id of the list. * @param listType The type of the list. * @param codehashes The codehashes to remove. */ function removeCodeHashesFromList(uint48 id, uint8 listType, bytes32[] calldata codehashes) external; ``` **Checking List and Collection Settings** ```solidity /** * @notice Returns the owner of the specified list id. */ function listOwners(uint48 id) external view returns (address); /** * @notice Get the security policy of the specified collection. * @param collection The address of the collection. * @return The security policy of the specified collection, which includes: * Ruleset id, list id, global options, ruleset-specific options, optional custom ruleset address, * and token type (if registered). */ function getCollectionSecurityPolicy(address collection) external view returns (CollectionSecurityPolicy memory); /** * @notice Get accounts by list id and list type. * @param id The id of the list. * @param listType The type of the list. * @return An array of accounts in the list of the specified type. */ function getListAccounts(uint48 id, uint8 listType) external view returns (address[] memory); /** * @notice Get codehashes by list id and list type. * @param id The id of the list. * @param listType The type of the list. * @return An array of codehashes in the list of the specified type. */ function getListCodeHashes(uint48 id, uint8 listType) external view returns (bytes32[] memory); /** * @notice Check if an account is found in a specified list id / list type. * @param id The id of the list. * @param listType The type of the list. * @param account The address of the account to check. * @return True if the account is in the specified list / type, false otherwise. */ function isAccountInList(uint48 id, uint8 listType, address account) external view returns (bool); /** * @notice Check if a codehash is in a specified list / type. * @param id The id of the list. * @param listType The type of the list. * @param codehash The codehash to check. * @return True if the codehash is in the specified list / type, false otherwise. */ function isCodeHashInList(uint48 id, uint8 listType, bytes32 codehash) external view returns (bool); /** * @notice Get accounts in list by collection and list type. * @param collection The address of the collection. * @param listType The type of the list. * @return An array of accounts. */ function getListAccountsByCollection(address collection, uint8 listType) external view returns (address[] memory); /** * @notice Get codehashes in list by collection and list type. * @param collection The address of the collection. * @param listType The type of the list. * @return An array of codehashes. */ function getListCodeHashesByCollection(address collection, uint8 listType) external view returns (bytes32[] memory); /** * @notice Check if an account is in the list by a specified collection and list type. * @param collection The address of the collection. * @param listType The type of the list. * @param account The address of the account to check. * @return True if the account is in the list / list type of the specified collection, false otherwise. */ function isAccountInListByCollection(address collection, uint8 listType, address account) external view returns (bool); /** * @notice Check if a codehash is in the list by a specified collection / list type. * @param collection The address of the collection. * @param listType The type of the list. * @param codehash The codehash to check. * @return True if the codehash is in the list / list type of the specified collection, false otherwise. */ function isCodeHashInListByCollection(address collection, uint8 listType, bytes32 codehash) external view returns (bool); ``` # Transfer Security (By Ruleset) As explained previously, different rulesets and ruleset options range from more permissive to less permissive. Depending upon the ruleset and options chosen, there can be workarounds implemented to attempt to bypass intended rules governing transfers. Defaults have been selected that strike the ideal balance of transfer security and ease of user experience. In case any of the rules are abused at scale, creators can update their ruleset and/or options to move to a higher level of security that thwarts more of these evasion techniques. In the following tables below, a checkmark denotes that the ruleset is secure from techniques the can be used to bypass transfer rules. ### Ruleset Vanilla | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | | | | | | | | | | ### Ruleset Blacklist | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | | | | | | | | | ### Ruleset Whitelist #### Configuration 1 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | None | ✔ | | | | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | ✔ | ✔ | #### Configuration 2 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO1 | ✔ | ✔ | | | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | | | #### Configuration 3 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO2 | ✔ | | ✔ | | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | ✔ | | #### Configuration 4 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO1 / WLO2 | ✔ | ✔ | ✔ | | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | | | #### Configuration 5 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO0 | | | | | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | ✔ | ✔ | ✔ | Limited | Limited | ✔ | ✔ | #### Configuration 6 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO3 | ✔ | | | ✔ | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | ✔ | ✔ | #### Configuration 7 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO1 / WLO3 | ✔ | ✔ | | ✔ | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | | | #### Configuration 8 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO2 / WLO3 | ✔ | | ✔ | ✔ | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | ✔ | | #### Configuration 9 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |--------------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WL01 / WLO2 / WLO3 | ✔ | ✔ | ✔ | ✔ | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | | | #### Configuration 10 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO0 / WLO3 | | | | ✔ | | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | ✔ | ✔ | ✔ | Limited | Limited | ✔ | ✔ | #### Configuration 11 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO4 | ✔ | | | | ✔ | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | ✔ | ✔ | #### Configuration 12 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WL01 / WLO4 | ✔ | ✔ | | | ✔ | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | ✔ | ✔ | Limited | | | | #### Configuration 13 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO2 / WLO4 | ✔ | | ✔ | | ✔ | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | ✔ | | #### Configuration 14 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |--------------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO1 / WL02 / WLO4 | ✔ | ✔ | ✔ | | ✔ | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | | | | | | | | #### Configuration 15 | Option Bits Set | Allows EOA OTC | Allows 7702 Delegate OTC | Allows Smart Wallet OTC | Blocks Receivers With Code | Blocks Unverified EOA Receivers | Blocks Unwhitelist Operators | |-----------------|----------------|--------------------------|-------------------------|----------------------------|---------------------------------|------------------------------| | WLO0 / WLO4 | | | | | ✔ | ✔ | | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | ✔ | ✔ | ✔ | Limited | Limited | ✔ | ✔ | ### Ruleset Soulbound | Blocked Exchange | Pop-Up Exchange | Offline OTC | Escrow Contract | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | EIP-7702 | Batch Tx Stuffing | |------------------|-----------------|-------------|-----------------|--------------------|---------------------------|----------------------------|----------|-------------------| | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ### Freezing Accounts As a method of last resort, creators can freeze an account to prevent all transfers to or from the address. This is intended to prevent malicious actors from circumventing any transfer rules the validators have set via unintended routes or exploits. Use of this feature is solely at the discretion of the token creator, and is disabled by default, requiring the token creator to opt in. This operation can be performed on the developer tools user interface, or using the following validator contract interfaces directly. ```solidity /** * @notice Adds accounts to the frozen accounts list of a collection. * * @dev Throws when the caller is neither collection contract, nor the owner or admin of the specified collection. * * @dev

Postconditions:

* 1. The accounts are added to the list of frozen accounts for a collection. * 2. A `AccountFrozenForCollection` event is emitted for each account added to the list. * * @param collection The address of the collection. * @param accountsToFreeze The list of accounts to added to frozen accounts. */ function freezeAccountsForCollection(address collection, address[] calldata accountsToFreeze) external; /** * @notice Removes accounts to the frozen accounts list of a collection. * * @dev Throws when the caller is neither collection contract, nor the owner or admin of the specified collection. * * @dev

Postconditions:

* 1. The accounts are removed from the list of frozen accounts for a collection. * 2. A `AccountUnfrozenForCollection` event is emitted for each account removed from the list. * * @param collection The address of the collection. * @param accountsToUnfreeze The list of accounts to remove from frozen accounts. */ function unfreezeAccountsForCollection(address collection, address[] calldata accountsToUnfreeze) external; /** * @notice Get frozen accounts by collection. * @param collection The address of the collection. * @return An array of frozen accounts. */ function getFrozenAccountsByCollection(address collection) external view returns (address[] memory); /** * @notice Check if an account is frozen for a specified collection. * @param collection The address of the collection. * @param account The address of the account to check. * @return True if the account is frozen by the specified collection, false otherwise. */ function isAccountFrozenForCollection(address collection, address account) external view returns (bool) { return validatorStorage().frozenAccounts[collection].nonEnumerableAccounts[account]; } ``` # Example Contracts Tokens implementing the Creator Token Standards will inherit from the Limit Break provided abstract contract implementations in the Creator Token Standards repository found [here](https://github.com/limitbreakinc/creator-token-standards). Creators may add additional logic to their token implementation as they would with other token contracts for token minting, burning, metadata rendering or other necessary functions. Creators should be careful when overriding functions implemented by the Creator Token Standard implementation contracts, such as `_beforeTokenTransfer`, to ensure that the Creator Token Standard logic is also executed by calling `super.[overriden function]([function parameters])`. ## ERC721-C ```solidity import {ERC721C} from '@limitbreak/creator-token-standards/erc721c/ERC721C.sol'; contract MockERC721C is ERC721C { // Your contract logic } ``` ## ERC721A-C ```solidity import {ERC721AC} from '@limitbreak/creator-token-standards/erc721c/ERC721AC.sol'; contract MockERC721AC is ERC721AC { // Your contract logic } ``` ## ERC1155-C ```solidity import {ERC1155C} from '@limitbreak/creator-token-standards/erc1155c/ERC1155C.sol'; contract MockERC1155C is ERC1155C { // Your contract logic } ``` ## ERC20-C ```solidity import {ERC20C} from '@limitbreak/creator-token-standards/erc20c/ERC20C.sol'; contract MockERC20C is ERC20C { // Your contract logic } ``` # EOA Verification A standalone contract name the `EOA Registry` that allows for user addresses to submit a signed message verifying that there is a private key associated with the account and that it is not an undeployed contract address. Off-chain order books *SHOULD* be aware of collections they are servicing that have set a transfer security level requiring the token recipient to be a verified EOA and follow the steps below to verify the user address with the collection's transfer validator. 1. Determine if the user's EOA has already been verified with the collection's transfer validator. This may be through the off-chain order book's indexed event logs or by calling the `isVerifiedEOA(address)` function. If the EOA has been verified, no further action is required. 2. Request a wallet signature using the `personal_sign` method with the message `EOA`. 3. Call either the `verifySignature(bytes)` or `verifySignatureVRS(uint8, bytes32, bytes32)` function from the user's EOA with the signature data obtained in step 2. This transaction will be recorded on-chain and require the user to pay gas. The `verifySignatureVRS` function is slightly more gas optimized and should be the default method utilized. ## EOA Registry Functions ### verifySignature Function called by an EOA with their signature data, as a bytes array, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignature(bytes calldata signature) external; ``` ### verifySignatureVRS Function called by an EOA with their signature, split into `v`, `r`, and `s` values for gas efficiency, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignatureVRS(uint8 v, bytes32 r, bytes32 s) external; ``` ### isVerifiedEOA Function called to check if a specified `account` has previously verified with the EOA Registry. ```solidity function isVerifiedEOA(address account) public view override returns (bool); ``` # Events Creator Token contracts emit several indexible events that should be indexed by off-chain order books. Off-chain order books *SHOULD* index the following events for collections they provide listing services for so that the order book can apply the appropriate transfer validation logic to orders such as obtaining an EOA signature from the recipient and registering it with the transfer validator EOA registry or filtering orders that are unfillable based on security settings. `Token Contract Events` are emitted by the collection's smart contract. `Transfer Validator Events` are emitted by transfer validator contracts. A default transfer validator has been deployed that collections *SHOULD* use, however creators *MAY* deploy alternative transfer validators. ## Token Contract Events ```solidity event TransferValidatorUpdated( address oldValidator, address newValidator); ``` ## Monitoring The Validator Integrators and other users can monitor updates to validator/collection settings through the following event interfaces. It is especially important for exchange platform to monitor settings of collections, as changes to ruleset or options can block access to unwanted exchanges or invalidate certain marketplace orders, rendering them unfillable. ### Ruleset Monitoring ```solidity /// @dev Emitted when a delegated ruleset is registered. event RulesetRegistered(address indexed delegatedRuleset); /// @dev Emitted when a ruleset binding is set. event RulesetBindingUpdated(uint8 indexed rulesetId, address indexed oldRuleset, address indexed newRuleset); ``` ### List Monitoring ```solidity /// @dev Emitted when a new list is created. event CreatedList(uint256 indexed id, string name); /// @dev Emitted when the ownership of a list is transferred to a new owner. event ReassignedListOwnership(uint256 indexed id, address indexed newOwner); /// @dev Emitted when an address is added to a list. event AddedAccountToList(uint8 indexed kind, uint48 indexed id, address indexed account); /// @dev Emitted when a codehash is added to a list. event AddedCodeHashToList(uint8 indexed kind, uint48 indexed id, bytes32 indexed codehash); /// @dev Emitted when an address is removed from a list. event RemovedAccountFromList(uint8 indexed kind, uint48 indexed id, address indexed account); /// @dev Emitted when a codehash is removed from a list. event RemovedCodeHashFromList(uint8 indexed kind, uint48 indexed id, bytes32 indexed codehash); ``` ### Collection Settings ```solidity /// @dev Emitted when a list is applied to a collection. event AppliedListToCollection(address indexed collection, uint48 indexed id); /// @dev Emitted when the validation ruleset id and/or custom ruleset for a collection is updated. event SetCollectionRuleset(address indexed collection, uint8 indexed rulesetId, address indexed customRuleset); /// @dev Emitted when a collection's token type is updated. event SetTokenType(address indexed collection, uint16 tokenType); /// @dev Emitted when a collection's security policy bit options is updated. event SetCollectionSecurityPolicyOptions(address indexed collection, uint8 globalOptions, uint16 rulesetOptions); /// @dev Emitted when a collection's security policy expansion words are updated. event SetCollectionExpansionWords(address indexed collection, bytes32 indexed key, bytes32 value); /// @dev Emitted when a collection's security policy expansion datums are updated. event SetCollectionExpansionDatums(address indexed collection, bytes32 indexed key, bytes value); ``` ### Frozen Accounts ```solidity /// @dev Emitted when an account is added to the list of frozen accounts for a collection. event AccountFrozenForCollection(address indexed collection, address indexed account); /// @dev Emitted when an account is removed from the list of frozen accounts for a collection. event AccountUnfrozenForCollection(address indexed collection, address indexed account); ``` # Creator Token Standards v4 # Overview ## 4.0.0 (2024-11-19) ### New Features * Support permissionless deployment of Creator Token Transfer Validator by any wallet on any EVM chain so it is freely deployable by the community. * Change collateral required for pausable Permit-C flags. 0.33 ETH on mainnet, 0.01 native tokens on all other chains. # Transfer Security Creator Token Standard contracts implement transfer security through the use of [Transfer Security Levels](#transfer-security-levels) and [Lists](#lists) giving creators full control over how their creations are distributed and used. ## Transfer Security Levels A collection's `Transfer Security Level` determines what restrictions are applied to transfers made by token owners. There are nine unique security levels with the default `Recommended` level being functionally equivalent to level `3` while optimizing contract initialization gas costs by leaving the storage slot as a zero value. | Level | List Type | OTC | Smart Contract Receivers | |-------------|-----------|---------|------------------------------------------| | Recommended | Whitelist | Allowed | Allowed | | 1 | None | Allowed | Allowed | | 2 | Blacklist | Allowed | Allowed | | 3 | Whitelist | Allowed | Allowed | | 4 | Whitelist | Blocked | Allowed | | 5 | Whitelist | Allowed | Blocked Using Code Length Check | | 6 | Whitelist | Allowed | Blocked Using EOA Signature Verification | | 7 | Whitelist | Blocked | Blocked Using Code Length Check | | 8 | Whitelist | Blocked | Blocked Using EOA Signature Verification | | 9 | None | Blocked | Blocked by restricting transfer | Transfer security settings are applied by a `Transfer Validator` contract. Limit Break provides a default transfer validator for use by collections. Creators *may* implement their own transfer validator however it is highly recommended to use the default for user experience and gas efficiency as it provides a single EOA Registry for all collections using Creator Token Standards and will minimize user friction on collections that apply security levels 6 or 8. Creators can manage their security settings at [https://developers.freenft.com](https://developers.freenft.com), with direct calls to their token contract (see [Token Contract Functions](#token-contract-functions)) or direct calls to the transfer validator (see [Transfer Contract Functions](#transfer-validator-functions)). ## List Type List types may be `None`, `Blacklist` or `Whitelist` and apply to the over-the-counter (OTC) transfer setting. - **None** is only applied to security level `1`. There are no filter lists applied to the transfer. - **Blacklist** is only applied to security level `2`. If the transaction caller's codehash or contract address has been added to the list the transaction will not be allowed. This type of list is easily bypassed by users that deploy the same contracts to new addresses or make small changes to the contract code to get a new codehash without significantly changing the contract logic. - **Whitelist** is applied to all other security levels. If the transaction caller's codehash or contract address has *not* been added to the list the transaction will not be allowed. This type of list allows a creator to specify exactly which contracts are allowed to transact items in the collection. ## Lists Lists contain contract addresses and codehashes curated by a list owner and are stored in the Transfer Validator that is utilized by the collection. Lists *may* be shared between collections. A list will be enforced during a token transfer transaction based on the security level set for the collection. ## OTC OTC may be set to `Allowed` or `Blocked` based on the security level selected. If OTC is `Allowed` then a token owner *may* transfer their tokens to another address without the transfer routing through another smart contract. If OTC is `Blocked` then a token owner's transfer *must* be routed through a smart contract. ## Smart Contract Receivers Smart Contract Receivers may be set to `Allowed` or `Blocked` with the blocked state enforcement set to `Code Length Check` or `EOA Signature Verification`. If smart contract receivers are `Allowed` then a token *may* be transferred to a smart contract. If smart contract receivers are `Blocked` then the transfer validator will check that the receiver is an EOA using contract code length or by requiring the receiver send a signed message to the EOA Registry to prove that their is a private key for the address. Code length checks may be bypassed by executing transactions from a smart contract's constructor code. EOA signature verification requires additional steps and a transaction by the receiver. ## Authorizers The Creator Token Transfer Validator now includes a new validation procedure, utilizing approved authorizers to review and approve transfers. Authorizers will provide a signature enabling transfers if the transaction meets certain criteria such as paying royalties, multi-hop transfers, or utilizing a limited smart contract system for gaming restraints. ## Account Freezing As a method of last resort, creators can freeze an account to prevent all transfers from the address. This is intended to prevent malicious actors from circumventing any transfer rules the validators have set via unintended routes or exploits. ## Mechanisms By Which Royalties Can Be Evaded (By Security Level) | Level | Blocked Exchange | Pop-Up Exchange | OTC / Escrow | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | |-------|------------------|-----------------|--------------|--------------------|---------------------------|----------------------------| | 1 | Yes | Yes | Yes | Yes | Yes | Yes | | 2 | No | Yes | Yes | Yes | Yes | Yes | | 3 | No | No | Yes | Yes | Yes | Yes | | 4 | No | No | No | Yes | Yes | Yes | | 5 | No | No | Yes | Limited | No | Yes | | 6 | No | No | Yes | No | No | Yes | | 7 | No | No | No | Limited | No | Limited | | 8 | No | No | No | No | No | Limited | | 9 | No | No | No | No | No | No | ## Transfer Security Functions Transfer security functions are located in two places for a token contract - the token contract itself and the transfer validator contract that the token contract has set as its transfer validator. ### Token Contract Functions The functions in this section are implemented in the token contract. #### setTransferValidator Callable by the token contract owner or any account that is assigned the default admin role to set the transfer validator to a new address. ```solidity function setTransferValidator(address transferValidator_) public; ``` ### Transfer Validator Functions The functions in this section are implemented in the transfer validator used by the token contract. #### createList Callable by any user to create a new list. ```solidity function createList(string calldata name) public returns (uint120); ``` #### createListCopy Callable by any user to create a new list copied from an existing list. ```solidity function createListCopy(string calldata name, uint120 sourceListId) external returns (uint120); ``` #### reassignOwnershipOfList Callable by the owner of a list to transfer ownership of the list to a new owner. ```solidity function reassignOwnershipOfList(uint120 id, address newOwner) public; ``` #### renounceOwnershipOfList Callable by the owner of a list to renounce ownership of the list and prevent further changes. ```solidity function renounceOwnershipOfList(uint120 id) public; ``` #### addAccountsToBlacklist Callable by the owner of a list to add the specified accounts to the list's blacklist. ```solidity function addAccountsToBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### addAccountsToWhitelist Callable by the owner of a list to add the specified accounts to the list's whitelist. ```solidity function addAccountsToWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### addCodeHashesToBlacklist Callable by the owner of a list to add the specified code hashes to the list's blacklist. ```solidity function addCodeHashesToBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### addCodeHashesToWhitelist Callable by the owner of a list to add the specified code hashes to the list's whitelist. ```solidity function addCodeHashesToWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### addAccountsToAuthorizers Callable by the owner of a list to add the specified authorizers to the list's approved authorizer addresses. ```solidity function addAccountsToAuthorizers( uint120 id, address[] calldata accounts ) external ``` #### removeAccountsFromBlacklist Callable by the owner of a list to remove the specified accounts from the list's blacklist. ```solidity function removeAccountsFromBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### removeAccountsFromWhitelist Callable by the owner of a list to remove the specified accounts from the list's whitelist. ```solidity function removeAccountsFromWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### removeCodeHashesFromBlacklist Callable by the owner of a list to remove the specified code hashes from the list's blacklist. ```solidity function removeCodeHashesFromBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeCodeHashesFromWhitelist Callable by the owner of a list to remove the specified code hashes from the list's whitelist. ```solidity function removeCodeHashesFromWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeAccountsFromAuthorizers Callable by the owner of a list to remove the specified authorizers from the list's approved authorizer addresses. ```solidity function removeAccountsFromAuthorizers( uint120 id, address[] calldata accounts ) external ``` #### setTransferSecurityLevelOfCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the token's transfer security level. The caller can also define authorizers, set if authorizers can enable wildcard allowances and enable freezing of accounts. ```solidity function setTransferSecurityLevelOfCollection( address collection, TransferSecurityLevels level, bool enableAuthorizationMode, bool authorizersCanSetWildcardOperators, bool enableAccountFreezingMode ) external; ``` #### applyListToCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the list to use when applying whitelist/blacklist transfer restrictions. ```solidity function applyListToCollection(address collection, uint120 id) public; ``` #### freezeAccountsForCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to freeze transferability for a specific set of addresses. ```solidity function freezeAccountsForCollection(address collection, address[] calldata accountsToFreeze) public; ``` #### unfreezeAccountsForCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to remove transfer restrictions from a specific set of addresses. ```solidity function unfreezeAccountsForCollection(address collection, address[] calldata accountsToUnfreeze) public; ``` # Example Contracts Tokens implementing the Creator Token Standards will inherit from the Limit Break provided abstract contract implementations in the Creator Token Standards repository found [here](https://github.com/limitbreakinc/creator-token-standards). Creators may add additional logic to their token implementation as they would with other token contracts for token minting, burning, metadata rendering or other necessary functions. Creators should be careful when overriding functions implemented by the Creator Token Standard implementation contracts, such as `_beforeTokenTransfer`, to ensure that the Creator Token Standard logic is also executed by calling `super.[overriden function]([function parameters])`. ## ERC721-C ```solidity import {ERC721C} from '@limitbreak/creator-token-standards/erc721c/ERC721C.sol'; contract MockERC721C is ERC721C { // Your contract logic } ``` ## ERC721A-C ```solidity import {ERC721AC} from '@limitbreak/creator-token-standards/erc721c/ERC721AC.sol'; contract MockERC721AC is ERC721AC { // Your contract logic } ``` ## ERC1155-C ```solidity import {ERC1155C} from '@limitbreak/creator-token-standards/erc1155c/ERC1155C.sol'; contract MockERC1155C is ERC1155C { // Your contract logic } ``` ## ERC20-C ```solidity import {ERC20C} from '@limitbreak/creator-token-standards/erc20c/ERC20C.sol'; contract MockERC20C is ERC20C { // Your contract logic } ``` # Events Creator Token contracts emit several indexible events that should be indexed by off-chain order books. Off-chain order books *SHOULD* index the following events for collections they provide listing services for so that the order book can apply the appropriate transfer validation logic to orders such as obtaining an EOA signature from the recipient and registering it with the transfer validator EOA registry or filtering orders that are unfillable based on security settings. `Token Contract Events` are emitted by the collection's smart contract. `Transfer Validator Events` are emitted by transfer validator contracts. A default transfer validator has been deployed that collections *SHOULD* use, however creators *MAY* deploy alternative transfer validators. ## Token Contract Events ```solidity event TransferValidatorUpdated( address oldValidator, address newValidator); ``` ## Transfer Validator Events ```solidity event SetTransferSecurityLevel( address indexed collection, TransferSecurityLevels level); event CreatedList( uint256 indexed id, string name); event AppliedListToCollection( address indexed collection, uint120 indexed id); event ReassignedListOwnership( uint256 indexed id, address indexed newOwner); event AddedAccountToList( ListTypes indexed kind, uint256 indexed id, address indexed account); event AddedCodeHashToList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event RemovedAccountFromList( ListTypes indexed kind, uint256 indexed id, address indexed account); event RemovedCodeHashFromList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event VerifiedEOASignature( address indexed account); event AppliedListToCollection( address indexed collection, uint120 indexed id); event AccountUnfrozenForCollection( address indexed collection, address indexed account); event SetAuthorizationModeEnabled( address indexed collection, bool enabled, bool authorizersCanSetWildcardOperators); event SetAccountFreezingModeEnabled( address indexed collection, bool enabled); ``` # Transfer Validation Off-chain order books should be aware of the transfer security level setting (see [Transfer Security](../for-creators/transfer-security)) for each collection they provide listing services for and apply filtering logic to hide listings that are unfillable. Situations where an off-chain order book will need to filter listings due to changes in transfer validation settings include: - No EOA Validation to EOA Validation - Security Level At Offer Creation: Recommended, 1, 2, 3, 4, 5, or 7 - New Security Level: 6 or 8 - Affected Orders: Offers by a maker where the beneficiary is not a verified EOA. - Action: Offer should be hidden until the beneficiary performs the [EOA Verification](eoa-verification) process. - Notification: Order book *should* notify the maker that they have orders that are unfillable pending action. - Smart Contract Receivers Allowed to Blocked - Security Level At Offer Creation: Recommended, 1, 2, 3, 4 - New Security Level: 5 or 7 - Affected Orders: Offers by a maker where the beneficiary has contract code. - Action: Offer should be hidden. - Notification: Order book *should* notify the maker that they have orders that are unfillable due to collection security settings. - Marketplace Contract Removed from Whitelist - Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for collections using the whitelist the marketplace contract was removed from. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is removed from a whitelist by the list owner or when a collection changes to a list that does not include the marketplace contract in its whitelist.* - Marketplace Contract Added to Blacklist - Security Level: 2 - Affected Orders: All orders for collections using the blacklist the marketplace contract was added to. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is added to a blacklist by the list owner or when a collection changes to a list that includes the marketplace contract in its blacklist.* - No List or Blacklist to Whitelist, Marketplace Contract Not On Whitelist - Security Level At Order Creation: 1, 2 - New Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - No List or Whitelist to Blacklist, Marketplace Contract On Blacklist - Security Level At Order Creation: Recommended, 1, 3, 4, 5, 6, 7, 8 - New Security Level: 2 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - Token updated to Soul Bound Token - Security Level: 9 - Affected Orders: All orders for the collection. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. # EOA Verification A standalone contract name the `EOA Registry` that allows for user addresses to submit a signed message verifying that there is a private key associated with the account and that it is not an undeployed contract address. Off-chain order books *SHOULD* be aware of collections they are servicing that have set a transfer security level requiring the token recipient to be a verified EOA and follow the steps below to verify the user address with the collection's transfer validator. 1. Determine if the user's EOA has already been verified with the collection's transfer validator. This may be through the off-chain order book's indexed event logs or by calling the `isVerifiedEOA(address)` function. If the EOA has been verified, no further action is required. 2. Request a wallet signature using the `personal_sign` method with the message `EOA`. 3. Call either the `verifySignature(bytes)` or `verifySignatureVRS(uint8, bytes32, bytes32)` function from the user's EOA with the signature data obtained in step 2. This transaction will be recorded on-chain and require the user to pay gas. The `verifySignatureVRS` function is slightly more gas optimized and should be the default method utilized. ## EOA Registry Functions ### verifySignature Function called by an EOA with their signature data, as a bytes array, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignature(bytes calldata signature) external; ``` ### verifySignatureVRS Function called by an EOA with their signature, split into `v`, `r`, and `s` values for gas efficiency, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignatureVRS(uint8 v, bytes32 r, bytes32 s) external; ``` ### isVerifiedEOA Function called to check if a specified `account` has previously verified with the EOA Registry. ```solidity function isVerifiedEOA(address account) public view override returns (bool); ``` # Contract Deployments Creator Token Transfer Validator v4.0.0 can be permissionlessly deployed by any wallet to the same addresses on any EVM compatible chain. #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Creator Token Transfer Validator V4.0.0 | `0x721C002B0059009a671D00aD1700c9748146cd1B` | | EOA Registry | `0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C` | ***Note: If Creator Token Transfer Validator is not yet deployed on your chain, use [the infrastructure deployment tool at developers.apptokens.com/infrastructure.](https://developers.apptokens.com/infrastructure)*** # Creator Token Standards v3 # Transfer Security Creator Token Standard contracts implement transfer security through the use of [Transfer Security Levels](#transfer-security-levels) and [Lists](#lists) giving creators full control over how their creations are distributed and used. ## Transfer Security Levels A collection's `Transfer Security Level` determines what restrictions are applied to transfers made by token owners. There are nine unique security levels with the default `Recommended` level being functionally equivalent to level `3` while optimizing contract initialization gas costs by leaving the storage slot as a zero value. | Level | List Type | OTC | Smart Contract Receivers | |-------------|-----------|---------|------------------------------------------| | Recommended | Whitelist | Allowed | Allowed | | 1 | None | Allowed | Allowed | | 2 | Blacklist | Allowed | Allowed | | 3 | Whitelist | Allowed | Allowed | | 4 | Whitelist | Blocked | Allowed | | 5 | Whitelist | Allowed | Blocked Using Code Length Check | | 6 | Whitelist | Allowed | Blocked Using EOA Signature Verification | | 7 | Whitelist | Blocked | Blocked Using Code Length Check | | 8 | Whitelist | Blocked | Blocked Using EOA Signature Verification | | 9 | None | Blocked | Blocked by restricting transfer | Transfer security settings are applied by a `Transfer Validator` contract. Limit Break provides a default transfer validator for use by collections. Creators *may* implement their own transfer validator however it is highly recommended to use the default for user experience and gas efficiency as it provides a single EOA Registry for all collections using Creator Token Standards and will minimize user friction on collections that apply security levels 6 or 8. Creators can manage their security settings at [https://developers.freenft.com](https://developers.freenft.com), with direct calls to their token contract (see [Token Contract Functions](#token-contract-functions)) or direct calls to the transfer validator (see [Transfer Contract Functions](#transfer-validator-functions)). ## List Type List types may be `None`, `Blacklist` or `Whitelist` and apply to the over-the-counter (OTC) transfer setting. - **None** is only applied to security level `1`. There are no filter lists applied to the transfer. - **Blacklist** is only applied to security level `2`. If the transaction caller's codehash or contract address has been added to the list the transaction will not be allowed. This type of list is easily bypassed by users that deploy the same contracts to new addresses or make small changes to the contract code to get a new codehash without significantly changing the contract logic. - **Whitelist** is applied to all other security levels. If the transaction caller's codehash or contract address has *not* been added to the list the transaction will not be allowed. This type of list allows a creator to specify exactly which contracts are allowed to transact items in the collection. ## Lists Lists contain contract addresses and codehashes curated by a list owner and are stored in the Transfer Validator that is utilized by the collection. Lists *may* be shared between collections. A list will be enforced during a token transfer transaction based on the security level set for the collection. ## OTC OTC may be set to `Allowed` or `Blocked` based on the security level selected. If OTC is `Allowed` then a token owner *may* transfer their tokens to another address without the transfer routing through another smart contract. If OTC is `Blocked` then a token owner's transfer *must* be routed through a smart contract. ## Smart Contract Receivers Smart Contract Receivers may be set to `Allowed` or `Blocked` with the blocked state enforcement set to `Code Length Check` or `EOA Signature Verification`. If smart contract receivers are `Allowed` then a token *may* be transferred to a smart contract. If smart contract receivers are `Blocked` then the transfer validator will check that the receiver is an EOA using contract code length or by requiring the receiver send a signed message to the EOA Registry to prove that their is a private key for the address. Code length checks may be bypassed by executing transactions from a smart contract's constructor code. EOA signature verification requires additional steps and a transaction by the receiver. ## Authorizers The Creator Token Transfer Validator now includes a new validation procedure, utilizing approved authorizers to review and approve transfers. Authorizers will provide a signature enabling transfers if the transaction meets certain criteria such as paying royalties, multi-hop transfers, or utilizing a limited smart contract system for gaming restraints. ## Account Freezing As a method of last resort, creators can freeze an account to prevent all transfers from the address. This is intended to prevent malicious actors from circumventing any transfer rules the validators have set via unintended routes or exploits. ## Mechanisms By Which Royalties Can Be Evaded (By Security Level) | Level | Blocked Exchange | Pop-Up Exchange | OTC / Escrow | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | |-------|------------------|-----------------|--------------|--------------------|---------------------------|----------------------------| | 1 | Yes | Yes | Yes | Yes | Yes | Yes | | 2 | No | Yes | Yes | Yes | Yes | Yes | | 3 | No | No | Yes | Yes | Yes | Yes | | 4 | No | No | No | Yes | Yes | Yes | | 5 | No | No | Yes | Limited | No | Yes | | 6 | No | No | Yes | No | No | Yes | | 7 | No | No | No | Limited | No | Limited | | 8 | No | No | No | No | No | Limited | | 9 | No | No | No | No | No | No | ## Transfer Security Functions Transfer security functions are located in two places for a token contract - the token contract itself and the transfer validator contract that the token contract has set as its transfer validator. ### Token Contract Functions The functions in this section are implemented in the token contract. #### setTransferValidator Callable by the token contract owner or any account that is assigned the default admin role to set the transfer validator to a new address. ```solidity function setTransferValidator(address transferValidator_) public; ``` ### Transfer Validator Functions The functions in this section are implemented in the transfer validator used by the token contract. #### createList Callable by any user to create a new list. ```solidity function createList(string calldata name) public returns (uint120); ``` #### createListCopy Callable by any user to create a new list copied from an existing list. ```solidity function createListCopy(string calldata name, uint120 sourceListId) external returns (uint120); ``` #### reassignOwnershipOfList Callable by the owner of a list to transfer ownership of the list to a new owner. ```solidity function reassignOwnershipOfList(uint120 id, address newOwner) public; ``` #### renounceOwnershipOfList Callable by the owner of a list to renounce ownership of the list and prevent further changes. ```solidity function renounceOwnershipOfList(uint120 id) public; ``` #### addAccountsToBlacklist Callable by the owner of a list to add the specified accounts to the list's blacklist. ```solidity function addAccountsToBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### addAccountsToWhitelist Callable by the owner of a list to add the specified accounts to the list's whitelist. ```solidity function addAccountsToWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### addCodeHashesToBlacklist Callable by the owner of a list to add the specified code hashes to the list's blacklist. ```solidity function addCodeHashesToBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### addCodeHashesToWhitelist Callable by the owner of a list to add the specified code hashes to the list's whitelist. ```solidity function addCodeHashesToWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### addAccountsToAuthorizers Callable by the owner of a list to add the specified authorizers to the list's approved authorizer addresses. ```solidity function addAccountsToAuthorizers( uint120 id, address[] calldata accounts ) external ``` #### removeAccountsFromBlacklist Callable by the owner of a list to remove the specified accounts from the list's blacklist. ```solidity function removeAccountsFromBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### removeAccountsFromWhitelist Callable by the owner of a list to remove the specified accounts from the list's whitelist. ```solidity function removeAccountsFromWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### removeCodeHashesFromBlacklist Callable by the owner of a list to remove the specified code hashes from the list's blacklist. ```solidity function removeCodeHashesFromBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeCodeHashesFromWhitelist Callable by the owner of a list to remove the specified code hashes from the list's whitelist. ```solidity function removeCodeHashesFromWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeAccountsFromAuthorizers Callable by the owner of a list to remove the specified authorizers from the list's approved authorizer addresses. ```solidity function removeAccountsFromAuthorizers( uint120 id, address[] calldata accounts ) external ``` #### setTransferSecurityLevelOfCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the token's transfer security level. The caller can also define authorizers, set if authorizers can enable wildcard allowances and enable freezing of accounts. ```solidity function setTransferSecurityLevelOfCollection( address collection, TransferSecurityLevels level, bool enableAuthorizationMode, bool authorizersCanSetWildcardOperators, bool enableAccountFreezingMode ) external; ``` #### applyListToCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the list to use when applying whitelist/blacklist transfer restrictions. ```solidity function applyListToCollection(address collection, uint120 id) public; ``` #### freezeAccountsForCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to freeze transferability for a specific set of addresses. ```solidity function freezeAccountsForCollection(address collection, address[] calldata accountsToFreeze) public; ``` #### unfreezeAccountsForCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to remove transfer restrictions from a specific set of addresses. ```solidity function unfreezeAccountsForCollection(address collection, address[] calldata accountsToUnfreeze) public; ``` # Example Contracts Tokens implementing the Creator Token Standards will inherit from the Limit Break provided abstract contract implementations in the Creator Token Standards repository found [here](https://github.com/limitbreakinc/creator-token-standards). Creators may add additional logic to their token implementation as they would with other token contracts for token minting, burning, metadata rendering or other necessary functions. Creators should be careful when overriding functions implemented by the Creator Token Standard implementation contracts, such as `_beforeTokenTransfer`, to ensure that the Creator Token Standard logic is also executed by calling `super.[overriden function]([function parameters])`. ## ERC721-C ```solidity import {ERC721C} from '@limitbreak/creator-token-standards/erc721c/ERC721C.sol'; contract MockERC721C is ERC721C { // Your contract logic } ``` ## ERC721A-C ```solidity import {ERC721AC} from '@limitbreak/creator-token-standards/erc721c/ERC721AC.sol'; contract MockERC721AC is ERC721AC { // Your contract logic } ``` ## ERC1155-C ```solidity import {ERC1155C} from '@limitbreak/creator-token-standards/erc1155c/ERC1155C.sol'; contract MockERC1155C is ERC1155C { // Your contract logic } ``` ## ERC20-C ```solidity import {ERC20C} from '@limitbreak/creator-token-standards/erc20c/ERC20C.sol'; contract MockERC20C is ERC20C { // Your contract logic } ``` # Transfer Validation Off-chain order books should be aware of the transfer security level setting (see [Transfer Security](../for-creators/transfer-security)) for each collection they provide listing services for and apply filtering logic to hide listings that are unfillable. Situations where an off-chain order book will need to filter listings due to changes in transfer validation settings include: - No EOA Validation to EOA Validation - Security Level At Offer Creation: Recommended, 1, 2, 3, 4, 5, or 7 - New Security Level: 6 or 8 - Affected Orders: Offers by a maker where the beneficiary is not a verified EOA. - Action: Offer should be hidden until the beneficiary performs the [EOA Verification](eoa-verification) process. - Notification: Order book *should* notify the maker that they have orders that are unfillable pending action. - Smart Contract Receivers Allowed to Blocked - Security Level At Offer Creation: Recommended, 1, 2, 3, 4 - New Security Level: 5 or 7 - Affected Orders: Offers by a maker where the beneficiary has contract code. - Action: Offer should be hidden. - Notification: Order book *should* notify the maker that they have orders that are unfillable due to collection security settings. - Marketplace Contract Removed from Whitelist - Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for collections using the whitelist the marketplace contract was removed from. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is removed from a whitelist by the list owner or when a collection changes to a list that does not include the marketplace contract in its whitelist.* - Marketplace Contract Added to Blacklist - Security Level: 2 - Affected Orders: All orders for collections using the blacklist the marketplace contract was added to. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is added to a blacklist by the list owner or when a collection changes to a list that includes the marketplace contract in its blacklist.* - No List or Blacklist to Whitelist, Marketplace Contract Not On Whitelist - Security Level At Order Creation: 1, 2 - New Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - No List or Whitelist to Blacklist, Marketplace Contract On Blacklist - Security Level At Order Creation: Recommended, 1, 3, 4, 5, 6, 7, 8 - New Security Level: 2 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - Token updated to Soul Bound Token - Security Level: 9 - Affected Orders: All orders for the collection. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. # Events Creator Token contracts emit several indexible events that should be indexed by off-chain order books. Off-chain order books *SHOULD* index the following events for collections they provide listing services for so that the order book can apply the appropriate transfer validation logic to orders such as obtaining an EOA signature from the recipient and registering it with the transfer validator EOA registry or filtering orders that are unfillable based on security settings. `Token Contract Events` are emitted by the collection's smart contract. `Transfer Validator Events` are emitted by transfer validator contracts. A default transfer validator has been deployed that collections *SHOULD* use, however creators *MAY* deploy alternative transfer validators. ## Token Contract Events ```solidity event TransferValidatorUpdated( address oldValidator, address newValidator); ``` ## Transfer Validator Events ```solidity event SetTransferSecurityLevel( address indexed collection, TransferSecurityLevels level); event CreatedList( uint256 indexed id, string name); event AppliedListToCollection( address indexed collection, uint120 indexed id); event ReassignedListOwnership( uint256 indexed id, address indexed newOwner); event AddedAccountToList( ListTypes indexed kind, uint256 indexed id, address indexed account); event AddedCodeHashToList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event RemovedAccountFromList( ListTypes indexed kind, uint256 indexed id, address indexed account); event RemovedCodeHashFromList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event VerifiedEOASignature( address indexed account); event AppliedListToCollection( address indexed collection, uint120 indexed id); event AccountUnfrozenForCollection( address indexed collection, address indexed account); event SetAuthorizationModeEnabled( address indexed collection, bool enabled, bool authorizersCanSetWildcardOperators); event SetAccountFreezingModeEnabled( address indexed collection, bool enabled); ``` # EOA Verification A standalone contract name the `EOA Registry` that allows for user addresses to submit a signed message verifying that there is a private key associated with the account and that it is not an undeployed contract address. Off-chain order books *SHOULD* be aware of collections they are servicing that have set a transfer security level requiring the token recipient to be a verified EOA and follow the steps below to verify the user address with the collection's transfer validator. 1. Determine if the user's EOA has already been verified with the collection's transfer validator. This may be through the off-chain order book's indexed event logs or by calling the `isVerifiedEOA(address)` function. If the EOA has been verified, no further action is required. 2. Request a wallet signature using the `personal_sign` method with the message `EOA`. 3. Call either the `verifySignature(bytes)` or `verifySignatureVRS(uint8, bytes32, bytes32)` function from the user's EOA with the signature data obtained in step 2. This transaction will be recorded on-chain and require the user to pay gas. The `verifySignatureVRS` function is slightly more gas optimized and should be the default method utilized. ## EOA Registry Functions ### verifySignature Function called by an EOA with their signature data, as a bytes array, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignature(bytes calldata signature) external; ``` ### verifySignatureVRS Function called by an EOA with their signature, split into `v`, `r`, and `s` values for gas efficiency, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignatureVRS(uint8 v, bytes32 r, bytes32 s) external; ``` ### isVerifiedEOA Function called to check if a specified `account` has previously verified with the EOA Registry. ```solidity function isVerifiedEOA(address account) public view override returns (bool); ``` # Contract Deployments #### Deployed Chains: - [Ethereum Mainnet](#ethereum) - [Base Mainnet](#base) - [Optimism Mainnet](#optimism) - [Arbitrum One Mainnet](#arbitrum) - [Polygon Mainnet](#polygon) - [Polygon zkEVM Mainnet](#polygon-zkevm) - [Avalanche C-Chain Mainnet](#avalanche-c-chain) - [Binance Smart Chain (BSC) Mainnet](#binance-smart-chain) - [Ethereum Sepolia Testnet](#ethereum-sepolia) #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Creator Token Transfer Validator V3 | `0x721C0078c2328597Ca70F5451ffF5A7B38D4E947` | | EOA Registry | `0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C` | ## Mainnet Deployments ### Ethereum - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://etherscan.io/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://etherscan.io/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Base - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://basescan.org/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://basescan.org/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Optimism - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://optimistic.etherscan.io/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - (0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C)[https://optimistic.etherscan.io/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C] ### Arbitrum - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://arbiscan.io/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://arbiscan.io/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Polygon - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://polygonscan.com/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://polygonscan.com/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Polygon zkEVM - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://zkevm.polygonscan.com/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://zkevm.polygonscan.com/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Binance Smart Chain - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://bscscan.com/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://bscscan.com/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ### Avalanche C-Chain - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://snowtrace.io/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://snowtrace.io/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) ## Testnet Deployments ### Ethereum Sepolia - Creator Token Transfer Validator V3 - [0x721C0078c2328597Ca70F5451ffF5A7B38D4E947](https://sepolia.etherscan.io/address/0x721C0078c2328597Ca70F5451ffF5A7B38D4E947) - EOA Registry - [0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C](https://sepolia.etherscan.io/address/0xE0A0004Dfa318fc38298aE81a666710eaDCEba5C) # Creator Token Standards v2 # Transfer Security Creator Token Standard contracts implement transfer security through the use of [Transfer Security Levels](#transfer-security-levels) and [Lists](#lists) giving creators full control over how their creations are distributed and used. ## Transfer Security Levels A collection's `Transfer Security Level` determines what restrictions are applied to transfers made by token owners. There are eight unique security levels with the default `Recommended` level being functionally equivalent to level `3` while optimizing contract initialization gas costs by leaving the storage slot as a zero value. | Level | List Type | OTC | Smart Contract Receivers | |-------------|-----------|---------|------------------------------------------| | Recommended | Whitelist | Allowed | Allowed | | 1 | None | Allowed | Allowed | | 2 | Blacklist | Allowed | Allowed | | 3 | Whitelist | Allowed | Allowed | | 4 | Whitelist | Blocked | Allowed | | 5 | Whitelist | Allowed | Blocked Using Code Length Check | | 6 | Whitelist | Allowed | Blocked Using EOA Signature Verification | | 7 | Whitelist | Blocked | Blocked Using Code Length Check | | 8 | Whitelist | Blocked | Blocked Using EOA Signature Verification | Transfer security settings are applied by a `Transfer Validator` contract. Limit Break provides a default transfer validator for use by collections. Creators *may* implement their own transfer validator however it is highly recommended to use the default for user experience and gas efficiency as it provides a single EOA Registry for all collections using Creator Token Standards and will minimize user friction on collections that apply security levels 6 or 8. Creators can manage their security settings at [https://developers.freenft.com](https://developers.freenft.com), with direct calls to their token contract (see [Token Contract Functions](#token-contract-functions)) or direct calls to the transfer validator (see [Transfer Contract Functions](#transfer-validator-functions)). ## List Type List types may be `None`, `Blacklist` or `Whitelist` and apply to the over-the-counter (OTC) transfer setting. - **None** is only applied to security level `1`. There are no filter lists applied to the transfer. - **Blacklist** is only applied to security level `2`. If the transaction caller's codehash or contract address has been added to the list the transaction will not be allowed. This type of list is easily bypassed by users that deploy the same contracts to new addresses or make small changes to the contract code to get a new codehash without significantly changing the contract logic. - **Whitelist** is applied to all other security levels. If the transaction caller's codehash or contract address has *not* been added to the list the transaction will not be allowed. This type of list allows a creator to specify exactly which contracts are allowed to transact items in the collection. ## Lists Lists contain contract addresses and codehashes curated by a list owner and are stored in the Transfer Validator that is utilized by the collection. Lists *may* be shared between collections. A list will be enforced during a token transfer transaction based on the security level set for the collection. ## OTC OTC may be set to `Allowed` or `Blocked` based on the security level selected. If OTC is `Allowed` then a token owner *may* transfer their tokens to another address without the transfer routing through another smart contract. If OTC is `Blocked` then a token owner's transfer *must* be routed through a smart contract. ## Smart Contract Receivers Smart Contract Receivers may be set to `Allowed` or `Blocked` with the blocked state enforcement set to `Code Length Check` or `EOA Signature Verification`. If smart contract receivers are `Allowed` then a token *may* be transferred to a smart contract. If smart contract receivers are `Blocked` then the transfer validator will check that the receiver is an EOA using contract code length or by requiring the receiver send a signed message to the EOA Registry to prove that their is a private key for the address. Code length checks may be bypassed by executing transactions from a smart contract's constructor code. EOA signature verification requires additional steps and a transaction by the receiver. ## Mechanisms By Which Royalties Can Be Evaded (By Security Level) | Level | Blocked Exchange | Pop-Up Exchange | OTC / Escrow | Wrapper Contracts | Trading Multi-Sig Wallets | Centralized Exchange w/EOA | |-------|------------------|-----------------|--------------|--------------------|---------------------------|----------------------------| | 1 | Yes | Yes | Yes | Yes | Yes | Yes | | 2 | No | Yes | Yes | Yes | Yes | Yes | | 3 | No | No | Yes | Yes | Yes | Yes | | 4 | No | No | No | Yes | Yes | Yes | | 5 | No | No | Yes | Limited | No | Yes | | 6 | No | No | Yes | No | No | Yes | | 7 | No | No | No | Limited | No | Limited | | 8 | No | No | No | No | No | Limited | ## Transfer Security Functions Transfer security functions are located in two places for a token contract - the token contract itself and the transfer validator contract that the token contract has set as its transfer validator. ### Token Contract Functions The functions in this section are implemented in the token contract. #### setToDefaultSecurityPolicy Callable by the token contract owner or any account that is assigned the default admin role to reset transfer security settings back to the Creator Token Standard defaults. ```solidity function setToDefaultSecurityPolicy() public; ``` #### setToCustomValidatorAndSecurityPolicy Callable by the token contract owner or any account that is assigned the default admin role to set a new transfer validator, transfer security level and specify a list for blacklist/whitelist restrictions. ```solidity function setToCustomValidatorAndSecurityPolicy( address validator, TransferSecurityLevels level, uint120 listId ) public; ``` #### setToCustomSecurityPolicy Callable by the token contract owner or any account that is assigned the default admin role to set the transfer security level and specify a list for blacklist/whitelist restrictions. ```solidity function setToCustomSecurityPolicy( TransferSecurityLevels level, uint120 listId ) public; ``` #### setTransferValidator Callable by the token contract owner or any account that is assigned the default admin role to set the transfer validator to a new address. ```solidity function setTransferValidator(address transferValidator_) public; ``` ### Transfer Validator Functions The functions in this section are implemented in the transfer validator used by the token contract. #### createList Callable by any user to create a new list. ```solidity function createList(string calldata name) public returns (uint120); ``` #### createListCopy Callable by any user to create a new list copied from an existing list. ```solidity function createListCopy(string calldata name, uint120 sourceListId) external returns (uint120); ``` #### reassignOwnershipOfList Callable by the owner of a list to transfer ownership of the list to a new owner. ```solidity function reassignOwnershipOfList(uint120 id, address newOwner) public; ``` #### renounceOwnershipOfList Callable by the owner of a list to renounce ownership of the list and prevent further changes. ```solidity function renounceOwnershipOfList(uint120 id) public; ``` #### addAccountsToBlacklist Callable by the owner of a list to add the specified accounts to the list's blacklist. ```solidity function addAccountsToBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### addAccountsToWhitelist Callable by the owner of a list to add the specified accounts to the list's whitelist. ```solidity function addAccountsToWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### addCodeHashesToBlacklist Callable by the owner of a list to add the specified code hashes to the list's blacklist. ```solidity function addCodeHashesToBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### addCodeHashesToWhitelist Callable by the owner of a list to add the specified code hashes to the list's whitelist. ```solidity function addCodeHashesToWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeAccountsFromBlacklist Callable by the owner of a list to remove the specified accounts from the list's blacklist. ```solidity function removeAccountsFromBlacklist( uint120 id, address[] calldata accounts ) external; ``` #### removeAccountsFromWhitelist Callable by the owner of a list to remove the specified accounts from the list's whitelist. ```solidity function removeAccountsFromWhitelist( uint120 id, address[] calldata accounts ) external; ``` #### removeCodeHashesFromBlacklist Callable by the owner of a list to remove the specified code hashes from the list's blacklist. ```solidity function removeCodeHashesFromBlacklist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### removeCodeHashesFromWhitelist Callable by the owner of a list to remove the specified code hashes from the list's whitelist. ```solidity function removeCodeHashesFromWhitelist( uint120 id, bytes32[] calldata codehashes ) external; ``` #### setTransferSecurityLevelOfCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the token's transfer security level. ```solidity function setTransferSecurityLevelOfCollection( address collection, TransferSecurityLevels level ) external; ``` #### applyListToCollection Callable by the token contract, owner of the token contract or any account that has been assigned the default admin role for the token contract to set the list to use when applying whitelist/blacklist transfer restrictions. ```solidity function applyListToCollection(address collection, uint120 id) public; ``` # Example Contracts Tokens implementing the Creator Token Standards will inherit from the Limit Break provided abstract contract implementations in the Creator Token Standards repository found [here](https://github.com/limitbreakinc/creator-token-standards). Creators may add additional logic to their token implementation as they would with other token contracts for token minting, burning, metadata rendering or other necessary functions. Creators should be careful when overriding functions implemented by the Creator Token Standard implementation contracts, such as `_beforeTokenTransfer`, to ensure that the Creator Token Standard logic is also executed by calling `super.[overriden function]([function parameters])`. ## ERC721-C ```solidity import {ERC721C} from '@limitbreak/creator-token-standards/erc721c/ERC721C.sol'; contract MockERC721C is ERC721C { // Your contract logic } ``` ## ERC721A-C ```solidity import {ERC721AC} from '@limitbreak/creator-token-standards/erc721c/ERC721AC.sol'; contract MockERC721AC is ERC721AC { // Your contract logic } ``` ## ERC1155-C ```solidity import {ERC1155C} from '@limitbreak/creator-token-standards/erc1155c/ERC1155C.sol'; contract MockERC1155C is ERC1155C { // Your contract logic } ``` ## ERC20-C ```solidity import {ERC20C} from '@limitbreak/creator-token-standards/erc20c/ERC20C.sol'; contract MockERC20C is ERC20C { // Your contract logic } ``` # Transfer Validation Off-chain order books should be aware of the transfer security level setting (see [Transfer Security](../for-creators/transfer-security)) for each collection they provide listing services for and apply filtering logic to hide listings that are unfillable. Situations where an off-chain order book will need to filter listings due to changes in transfer validation settings include: - No EOA Validation to EOA Validation - Security Level At Offer Creation: Recommended, 1, 2, 3, 4, 5, or 7 - New Security Level: 6 or 8 - Affected Orders: Offers by a maker where the beneficiary is not a verified EOA. - Action: Offer should be hidden until the beneficiary performs the [EOA Verification](eoa-verification) process. - Notification: Order book *should* notify the maker that they have orders that are unfillable pending action. - Smart Contract Receivers Allowed to Blocked - Security Level At Offer Creation: Recommended, 1, 2, 3, 4 - New Security Level: 5 or 7 - Affected Orders: Offers by a maker where the beneficiary has contract code. - Action: Offer should be hidden. - Notification: Order book *should* notify the maker that they have orders that are unfillable due to collection security settings. - Marketplace Contract Removed from Whitelist - Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for collections using the whitelist the marketplace contract was removed from. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is removed from a whitelist by the list owner or when a collection changes to a list that does not include the marketplace contract in its whitelist.* - Marketplace Contract Added to Blacklist - Security Level: 2 - Affected Orders: All orders for collections using the blacklist the marketplace contract was added to. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - *Note: This situation may occur when a marketplace contract is added to a blacklist by the list owner or when a collection changes to a list that includes the marketplace contract in its blacklist.* - No List or Blacklist to Whitelist, Marketplace Contract Not On Whitelist - Security Level At Order Creation: 1, 2 - New Security Level: Recommended, 3, 4, 5, 6, 7, 8 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. - No List or Whitelist to Blacklist, Marketplace Contract On Blacklist - Security Level At Order Creation: Recommended, 1, 3, 4, 5, 6, 7, 8 - New Security Level: 2 - Affected Orders: All orders for the collection using the marketplace contract. - Action: Orders should be hidden. - Notification: Order book *should* notify makers that they have orders that are unfillable due to collection security settings. # Events Creator Token contracts emit several indexible events that should be indexed by off-chain order books. Off-chain order books *SHOULD* index the following events for collections they provide listing services for so that the order book can apply the appropriate transfer validation logic to orders such as obtaining an EOA signature from the recipient and registering it with the transfer validator EOA registry or filtering orders that are unfillable based on security settings. `Token Contract Events` are emitted by the collection's smart contract. `Transfer Validator Events` are emitted by transfer validator contracts. A default transfer validator has been deployed that collections *SHOULD* use, however creators *MAY* deploy alternative transfer validators. ## Token Contract Events ```solidity event TransferValidatorUpdated( address oldValidator, address newValidator); ``` ## Transfer Validator Events ```solidity event SetTransferSecurityLevel( address indexed collection, TransferSecurityLevels level); event CreatedList( uint256 indexed id, string name); event AppliedListToCollection( address indexed collection, uint120 indexed id); event ReassignedListOwnership( uint256 indexed id, address indexed newOwner); event AddedAccountToList( ListTypes indexed kind, uint256 indexed id, address indexed account); event AddedCodeHashToList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event RemovedAccountFromList( ListTypes indexed kind, uint256 indexed id, address indexed account); event RemovedCodeHashFromList( ListTypes indexed kind, uint256 indexed id, bytes32 indexed codehash); event VerifiedEOASignature( address indexed account); ``` # EOA Verification Transfer validator contracts include an `EOA Registry` that allows for user addresses to submit a signed message verifying that there is a private key associated with the account and that it is not an undeployed contract address. Off-chain order books *SHOULD* be aware of collections they are servicing that have set a transfer security level requiring the token recipient to be a verified EOA and follow the steps below to verify the user address with the collection's transfer validator. 1. Determine if the user's EOA has already been verified with the collection's transfer validator. This may be through the off-chain order book's indexed event logs or by calling the `isVerifiedEOA(address)` function. If the EOA has been verified, no further action is required. 2. Request a wallet signature using the `personal_sign` method with the message `EOA`. 3. Call either the `verifySignature(bytes)` or `verifySignatureVRS(uint8, bytes32, bytes32)` function from the user's EOA with the signature data obtained in step 2. This transaction will be recorded on-chain and require the user to pay gas. The `verifySignatureVRS` function is slightly more gas optimized and should be the default method utilized. ## EOA Registry Functions ### verifySignature Function called by an EOA with their signature data, as a bytes array, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignature(bytes calldata signature) external; ``` ### verifySignatureVRS Function called by an EOA with their signature, split into `v`, `r`, and `s` values for gas efficiency, to verify their address is an EOA. The EOA Registry checks the signature against a predefined value and if it is valid updates the verified EOA mapping to mark the account as a verified EOA. ```solidity function verifySignatureVRS(uint8 v, bytes32 r, bytes32 s) external; ``` ### isVerifiedEOA Function called to check if a specified `account` has previously verified with the EOA Registry. ```solidity function isVerifiedEOA(address account) public view override returns (bool); ``` # Contract Deployments #### Deployed Chains: - [Ethereum Mainnet](#ethereum) - [Base Mainnet](#base) - [Optimism Mainnet](#optimism) - [Arbitrum One Mainnet](#arbitrum) - [Polygon Mainnet](#polygon) - [Polygon zkEVM Mainnet](#polygon-zkevm) - [Avalanche C-Chain Mainnet](#avalanche-c-chain) - [Binance Smart Chain (BSC) Mainnet](#binance-smart-chain) - [Ethereum Sepolia Testnet](#ethereum-sepolia) #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Creator Token Transfer Validator V2 | `0x721C00182a990771244d7A71B9FA2ea789A3b433` | ## Mainnet Deployments ### Ethereum - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://etherscan.io/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Base - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://basescan.org/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Optimism - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://optimistic.etherscan.io/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Arbitrum - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://arbiscan.io/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Polygon - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://polygonscan.com/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Polygon zkEVM - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://zkevm.polygonscan.com/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Binance Smart Chain - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://bscscan.com/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ### Avalanche C-Chain - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://snowtrace.io/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) ## Testnet Deployments ### Ethereum Sepolia - Creator Token Transfer Validator V2 - [0x721C00182a990771244d7A71B9FA2ea789A3b433](https://sepolia.etherscan.io/address/0x721C00182a990771244d7A71B9FA2ea789A3b433) # Payment Processor # Overview Payment Processor is a peer-to-peer NFT trading protocol. It is designed to serve as the blockchain execution layer for one or more off-chain order books and exchanges. In the simplest terms, an NFT orderbook can be thought of as an off-chain system with a minimum of two components: 1. Data store(s) for NFT order data and order maker signatures. 2. API(s) that provides secure read/write access to NFT order data. An NFT exchange is a user-facing interface where buyers and sellers can conduct NFT trading. An NFT exchange typically integrates with one or more NFT orderbook APIs to allow for order creation and order discoverability. NFT exchanges may be scoped to a single NFT collection, a single community consisting of related NFT collections, or a completely open platform for trading of all NFT collections. NFT exchanges run the gamut between a simple trading interface and a professional, feature-rich trading interface. Together, orderbooks and exchanges allow market makers to create liquidity in the form of listings and offers for NFTs. Market takers, then, may instantly execute and fill trades against order book liquidity in order to buy and sell. ## Limit Break's Position on Royalties Unlike other NFT trading protocols, NFT royalty enforcement is built directly into Payment Processor. Payment Processor will ***NEVER*** withhold creator royalties when they are configured on-chain. There are two ways creators can configure on-chain royalties on their collections that will be honored by Payment Processor. 1. Program royalties into the NFT smart contract using EIP-2981 2. Backfill royalties on Payment Processor for collections that lack EIP-2981 support By building NFTs using Limit Break's novel ERC721-C standard, creators have the power to guarantee that trades execute in the safety of the Payment Processor trading protocol where the creator, not the exchange, sets the rules for trading. *Note: To configure backfill royalties for collections that do not support EIP-2981 the collection must either be Ownable with the call to set royalties made by the owner or implement AccessControl with the call to set royalties made by an account with the default admin role.* *Note: If a collection does not implement EIP-2981, does not have an owner and does not have access control, royalties may still be set through an off-chain agreement with a marketplace with the marketplace using the fallback royalty recipient to deliver royalty payments to the creator.* ## Unparalleled Control For Creators Payment Processor protects the interests of creators, offering them choices no other exchange protocol has ever provided. ### Control Over Payment Methods and Pricing Historically, marketplaces have dictated the accepted payment methods for NFT trades. Payment Processor flips control to creators, putting them in the driver’s seat to set what methods of payment are permitted for trades on their collections. The spectrum of choices are: - **Default Payment Method Whitelist** — Rather than allow any payment method by default, all collections are automatically protected from trading in meme coin or other undesirable coins unless otherwise specified by the collection’s creator. This protects creators from trades where royalties are paid in worthless, undesirable coins. The defaults include: Native Currency (ETH or Equivalent), Wrapped Native Coin, Wrapped ETH (when applicable), USDC (Native), USDC (Bridged). - **Allow Any Payment Method** — For creators that wish to accept trades and royalties in any currency, the option to remove all restrictions is available. - **Custom Payment Method Whitelist** — Creators may define their own shared list of coins that may be used to trade their collections. This works great for Web3 games that have their own in-game currencies, as they can allow trading only in their game’s coin(s). - **Pricing Constraints** — Creators may choose a single payment method and apply a minimum floor or maximum ceiling price at the individual token id or collection level. This is great for Web3 games with special ultra-rare items as it offers some control over the economic balance of a game. ### Royalty Bounties Creators may choose to offer a royalty bounty. This is a percentage of royalties creators may choose to share with marketplaces. Royalty bounties are completely voluntary and set by a collection’s creator if they wish. Consider this simple scenario: An NFT trades for 1E where the creator royalty is 10%, royalty bounty is 20%, and marketplace fee = 1%. The creator receives 0.08E, the marketplace receives 0.03E (0.01E standard fee + 0.02E royalty bounty), and the seller receives 0.89E. Why would a creator volunteer to share royalties with a marketplace? Royalty bounties offer an incentive to align the interests of exchanges and creators. By offering a share of enforceable creator royalties to exchanges, or perhaps an exclusive partner exchange, it offers a new way to incentivize exchanges to cross-promote or boost the visibility of a collection for a period of time. ### Royalty Backfill Many older collections may not implement EIP-2981 royaltyInfo. Some of these collections have exchange blocking features, while others do not. Regardless of whether or not a collection can block exchanges and guarantee trades occur within Payment Processor, if a creator designates a backfilled royalty receiver and percentage for their collections it is guaranteed to be honored and enforced on all trades flowing through the Payment Processor. When combined with the power of ERC721-C NFTs with whitelisted transfers, the on-chain rules selected by creators cannot be circumvented!!! ### Trusted Channels Trusted channels allow exchanges to gain attribution for Payment Processor trades, where every exchange can create their own optional channel. Additionally, creators can choose whether or not to allow all channels or create an allow list of trusted channels. By using an allowlist of trusted channels, creators get access to controls to block undesirable Web2 applications from trading their work, which will be a popular feature for games. ## Continued Reading If you are a creator, configure your collection's Payment Processor trading settings by heading over to [https://developers.apptokens.com](https://developers.apptokens.com)! If you are an exchange, continue on to the [Exchange Integration Guide](./v3.0/for-exchanges/glossary). # Payment Processor V3 # Overview ## 3.0.0 (2024-11-19) ### New Features * Support permissionless deployment of Payment Processor by any wallet on any EVM chain so it is freely deployable by the community. * Add Limit Break's deterministic, permissionlessly deployable Wrapped Native token as a default payment method. * Added Bulk Order Signing - listings and bids may be bulk signed. A bulk listing or bulk bid may include up to 1024 individual orders signed by the maker with a single signature. * Added Permit-C Integration - sellers may optionally list NFTs using Permit-C approvals vs of `approvalForAll`. When accepting bids, the seller also has the option to sign a permit as part of the bid acceptance process or use `approvalForAll`. This can increase asset security for collectors. * Added ability for creators to specify royalty backfill as the primary source of royalties - this saves gas querying collections that do not implement 2981 royalties, and it allows collections that have an unused `royaltyInfo` implementation to collect royalties using the backfill instead. * Added Creator Settings Lazy Loading - from V3.0.0 onwards, creators will no longer have to apply their settings to every new Payment Processor instance, as they will be loaded one-time automatically from a settings registry upon the first trade on each collection. Lists such as custom payment method whitelists, trusted channel lists, and token-level pricing bounds are loaded and cached locally on an as-needed basis. * Add Tstorish re-entrancy guard for gas-cost effective, more comprehensive reentrancy protections while allowing down stream payment composability. This allowed removal of native push payment gas limits and gas limit overrides and also allowed native refunds to be securely issued directly instead of wrapping refunds. * Add mutable protocol fee that can be adjusted up or down by permissioned `FEE_MANAGER` role, based on market conditions or to initiate a migration to future versions of Payment Processor. Protocol fee versioning system allows for fee version grace periods to ensure orders are not instantly invalidated and that exchange integrators have built-in time to integrate new versions. * Unless otherwise adjusted after deployment, the protocol fees are initialized by default to 0.25% minimum protocol fee, 15% tax on exchange fees, and 25% tax on fee on top. When exchange fee tax is insufficient to cover the minimum, the shortfall is deducted from the seller's proceeds. When the exchange fee tax fully covers the minimum protocol fee, the seller's proceeds are unaffected. * Exchanges can stay current on protocol fees by subscribing to the new `ProtocolFeesUpdated` event. ### Interface Changes * Domain Separator has been removed from all trading function calldata. Instead, domain separator is looked up from a cached immutable variable, making trades more gas efficient. Marketplaces/exchanges should use the new v3.0.0 `PaymentProcessorEncoder` contract when generating calldata to ensure compatibility. * Added a collection-level only Pricing Constraints payment type. This saves gas for collections that do not use token-level min/max floors. * Remove banned accounts feature - this is moving to the core Creator Token Standards in order to persist across marketplace versions. * Calldata format has changed for all trading functions - use the new v3.0.0 `PaymentProcessorEncoder` to ensure compatibility. * Order digest for partial fills no longer includes the full EIP-712 type hash and domain separator. Instead, partially fillable ERC1155 orders are now tracked solely through the hash of the order data itself. * Replace `bool isCollectionLevelOrder` field in offers with `uint256 offerType` field, simplifying offer processing logic. * Remove `bytes32 rootHash` field from `TokenSetProof` struct, as it is more gas efficient to simply compute the `rootHash` during proof verification than passing it in via calldata. * Add `uint256 protocolFeeVersion` field to EIP-712 typed data hashes (`SALE_APPROVAL_HASH`, `PERMITTED_TRANSFER_SALE_APPROVAL`, `PERMITTED_ORDER_SALE_APPROVAL`, all `BULK_SALE_APPROVAL` type hashes). * Add `uint256 protocolFeeVersion` field to `Order` struct. * Cap item prices at type(uint240).max. This should not impact operations but allows more gas efficient math when computing payment splits. * ***Note: These breaking interface changes result in new trading function selectors and an update to calldata encoding, so use latest `PaymentProcessorEncoder`.*** ##### ***Payment Processor Encoder - Modified Functions*** * `encodeBuyListingCalldata` * `encodeAcceptOfferCalldata` * `encodeBulkBuyListingsCalldata` * `encodeBulkAcceptOffersCalldata` * `encodeSweepCollectionCalldata` ##### ***Payment Processor Encoder - Added Functions*** * `encodeBuyListingAdvancedCalldata` * `encodeAcceptOfferAdvancedCalldata` * `encodeBulkBuyListingsAdvancedCalldata` * `encodeBulkAcceptOffersAdvancedCalldata` * `encodeSweepCollectionAdvancedCalldata` ##### ***Payment Processor Encoder - Removed Functions*** * `encodeCreatePaymentMethodWhitelistCalldata` * `encodeReassignOwnershipOfPaymentMethodWhitelistCalldata` * `encodeRenounceOwnershipOfPaymentMethodWhitelistCalldata` * `encodeWhitelistPaymentMethodCalldata` * `encodeUnwhitelistPaymentMethodCalldata` * `encodeSetCollectionPaymentSettingsCalldata` * `encodeSetCollectionPricingBoundsCalldata` * `encodeSetTokenPricingBoundsCalldata` * `encodeAddTrustedChannelForCollectionCalldata` * `encodeRemoveTrustedChannelForCollectionCalldata` * `encodeAddBannedAccountForCollectionCalldata` * `encodeRemoveBannedAccountForCollectionCalldata` #### **EIP-712 Typehashes** The following type hashes were modified or added. ##### ***Modified Typehashes*** ```solidity // SALE_APPROVAL_HASH - Added protocol fee version field to be acknowledged by seller // v2.0.1 // 0x938786a8256d04dc45d6d5b997005aa07c0c9e3e4925d0d6c33128d240096ebc SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce) // v3.0.0 // 0x36e5aab0dde52c07206863cf8fe7b4b1a25be63a3ae5286f8fe80d159c154c4f SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion) ``` ##### ***Added Typehashes*** * Added PermitC compliant sale approval typehashes. See docs for details. * Added bulk order typehashes to support signing up to 1024 orders with a single signature. Read docs for details. #### **Events** The following events were modified, added or removed. ##### ***Modified Events*** ```solidity // OrderDigestOpened - Changed `orderStartAmount` from uint248 to uint256 // v2.0.1 event OrderDigestOpened( bytes32 indexed orderDigest, address indexed account, uint248 orderStartAmount); // v3.0.0 event OrderDigestOpened( bytes32 indexed orderDigest, address indexed account, uint256 orderStartAmount); // OrderDigestItemsFilled - Changed `amountFilled` from uint248 to uint256 // v2.0.1 event OrderDigestItemsFilled( bytes32 indexed orderDigest, address indexed account, uint248 amountFilled); // v3.0.0 event OrderDigestItemsFilled( bytes32 indexed orderDigest, address indexed account, uint256 amountFilled); // OrderDigestItemsRestored - Changed `amountRestoredToOrder` from uint248 to uint256 // v2.0.1 event OrderDigestItemsRestored( bytes32 indexed orderDigest, address indexed account, uint248 amountRestoredToOrder); // v3.0.0 event OrderDigestItemsRestored( bytes32 indexed orderDigest, address indexed account, uint256 amountRestoredToOrder); // OrderDigestItemsRestored - Replaced `blockBannedAccounts` with `useRoyaltyBackfillAsRoyaltySource` // v2.0.1 event UpdatedCollectionPaymentSettings( address indexed tokenAddress, PaymentSettings paymentSettings, uint32 indexed paymentMethodWhitelistId, address indexed constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool blockBannedAccounts); // v3.0.0 event UpdatedCollectionPaymentSettings( address indexed tokenAddress, PaymentSettings paymentSettings, uint32 indexed paymentMethodWhitelistId, address indexed constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool useRoyaltyBackfillAsRoyaltySource); ``` ##### ***Added Events*** ```solidity event PermittedOrderNonceInvalidated( uint256 indexed permitNonce, uint256 indexed orderNonce, address indexed account, bool wasCancellation); event ProtocolFeesUpdated( address indexed newProtocolFeeRecipient, uint16 minimumProtocolFeeBps, uint16 marketplaceFeeProtocolTaxBps, uint16 feeOnTopProtocolTaxBps, uint48 gracePeriodExpiration); event TrustedPermitProcessorAdded(address indexed permitProcessor); event TrustedPermitProcessorRemoved(address indexed permitProcessor); ``` ##### ***Removed Events*** ```solidity event BannedAccountAddedForCollection( address indexed tokenAddress, address indexed account); event BannedAccountRemovedForCollection( address indexed tokenAddress, address indexed account); ``` ### **Bug Fixes** * Fix burning of order nonce/partial fill order digest when NFT transfer fails. * Fix edge case caused by fallback functions. ### **Gas Optimizations** * Removed domain separator from calldata and used cached immutable domain separator instead. * Replace OpenZeppelin SafeERC20 library with new Limit Break ERC20 transfer library. * Refactor payments for fee on top to optimize payout flow for protocol fees. * Limit item price to `type(uint240).max` to allow for unchecked math in payment split computation. * Change diamond storage slot from the hash of a string value to `0x9A1D`. * Allow malleable signatures since order replay is prevented by order hash. * Replace standard ABI encoding/hashing with new Limit Break Efficient Hash library. * Remove token set proof root hash from calldata, calculate based on item + proof and validate with offer signature. * Change optimization runs from 30,000 to 40,000. * Other various gas optimizations for trading flows. # Settings Registry Payment Processor v3.0.0 introduces a new way of managing a collection's settings, through the `CollectionSettingsRegistry` contract (Settings Registry). Settings Registry is a central registry contract that will be shared across future versions of Payment Processor v3.X. The simplest way to manage your collection's settings is through [developers.apptokens.com](https://developers.apptokens.com). For creators that would like to build their own integrations for managing collection settings - the sections below describe the functions and parameters that Payment Processor uses to apply rules to collection orders. ### Account Freezing With the update to Payment Processor v3.0.0, the account freezing functions have been moved to the Creator Token Transfer Validator to prevent exploits via contracts or other forms of transfer validator circumvention. To set the frozen account list, see the documentation for [Creator Token Standards v3](../../../creator-token-standards/v3/for-creators/transfer-security#account-freezing). ## Settings Registry Functions ### createPaymentMethodWhitelist Callable by any account to create a new payment method whitelist. The whitelist will be owned by the address that calls the function. ```solidity /** * @notice Allows any user to create a new custom payment method whitelist. * * @dev

Postconditions:

* @dev 1. The payment method whitelist id tracker has been incremented by `1`. * @dev 2. The caller has been assigned as the owner of the payment method whitelist. * @dev 3. A `CollectionSettingsRegistry__CreatedPaymentMethodWhitelist` event has been emitted. * * @param whitelistName The name of the payment method whitelist. * @return paymentMethodWhitelistId The id of the newly created payment method whitelist. */ function createPaymentMethodWhitelist( string calldata whitelistName ) external returns (uint32 paymentMethodWhitelistId); ``` ### reassignOwnershipOfPaymentMethodWhitelist Callable by the owner of the whitelist to assign ownership of the whitelist to a new account. ```solidity /** * @notice Transfer ownership of a payment method whitelist list to a new owner. * * @dev Throws when the new owner is the zero address. * @dev Throws when the caller does not own the specified list. * * @dev

Postconditions:

* 1. The payment method whitelist list ownership is transferred to the new owner. * 2. A `ReassignedPaymentMethodWhitelistOwnership` event is emitted. * * @param id The id of the payment method whitelist. * @param newOwner The address of the new owner. */ function reassignOwnershipOfPaymentMethodWhitelist( uint32 id, address newOwner ) external; ``` ### renounceOwnershipOfPaymentMethodWhitelist Callable by the owner of the whitelist to renounce ownership of the whitelist. Whitelists with the owner renounced are immutable and can not be modified. ```solidity /** * @notice Renounce the ownership of a payment method whitelist, rendering the list immutable. * * @dev Throws when the caller does not own the specified list. * * @dev

Postconditions:

* 1. The ownership of the specified payment method whitelist is renounced. * 2. A `ReassignedPaymentMethodWhitelistOwnership` event is emitted. * * @param id The id of the payment method whitelist. */ function renounceOwnershipOfPaymentMethodWhitelist(uint32 id) external; ``` ### whitelistPaymentMethod Callable by the owner of the whitelist to add a new payment method. ```solidity /** * @notice Allows custom payment method whitelist owners to approve a new coin for use as a payment currency. * * @dev Throws when caller is not the owner of the specified payment method whitelist. * @dev Throws when the specified coin is already whitelisted under the specified whitelist id. * * @dev

Postconditions:

* @dev 1. `paymentMethod` has been approved in `paymentMethodWhitelist` mapping. * @dev 2. A `PaymentMethodAddedToWhitelist` event has been emitted. * * @param paymentMethodWhitelistId The id of the payment method whitelist to update. * @param paymentMethods The address of the payment method to whitelist. */ function whitelistPaymentMethod( uint32 paymentMethodWhitelistId, address[] calldata paymentMethods, address[] calldata paymentProcessorsToSync ) external; ``` ### unwhitelistPaymentMethod Callable by the owner of the whitelist to remove a payment method. ```solidity /** * @notice Allows custom payment method whitelist owners to remove a coin from the list of approved payment * currencies. * * @dev Throws when caller is not the owner of the specified payment method whitelist. * @dev Throws when the specified coin is not currently whitelisted under the specified whitelist id. * * @dev

Postconditions:

* @dev 1. `paymentMethod` has been removed from the `paymentMethodWhitelist` mapping. * @dev 2. A `CollectionSettingsRegistry__PaymentMethodRemovedFromWhitelist` event has been emitted. * * @param paymentMethodWhitelistId The id of the payment method whitelist to update. * @param paymentMethods The address of the payment method to unwhitelist. */ function unwhitelistPaymentMethod( uint32 paymentMethodWhitelistId, address[] calldata paymentMethods, address[] calldata paymentProcessorsToSync ) external; ``` ### syncRemovedPaymentMethodsFromWhitelist Callable by any user. This function will force a sync of the removed payment methods from a whitelist. ```solidity /** * @notice Forces sync of removed payment methods from the whitelist. This is unowned to allow for anyone to trigger updates of removed payment methods. * * @dev Leaving this unowned allows for anyone to trigger the removal of payment methods from the whitelist. * @dev If there is an issue with a payment method, this allows for a quick removal from all payment processors. * @dev Throws when the specified payment method is still whitelisted under the specified whitelist id. * * @dev

Postconditions:

* @dev 1. `paymentMethod` has been removed from each payment processor in the `paymentProcessorsToSync` array. * @dev 2. X * Y `PaymentMethodRemovedFromWhitelist` events have been emitted where X is the number of payment methods and Y is the number of payment processors to sync. * * @param paymentMethodWhitelistId The id of the payment method whitelist to update. * @param paymentMethods An array of payment method addresses to remove. * @param paymentProcessorsToSync An array of the payment processors to sync the removal with. */ function syncRemovedPaymentMethodsFromWhitelist( uint32 paymentMethodWhitelistId, address[] calldata paymentMethods, address[] calldata paymentProcessorsToSync ) external; ``` ### addTrustedPermitProcessors Callable by the owner of the default payment method whitelist to add a new permit processor instance. ```solidity /** * @notice Allows the trusted permit processor list owner to add a new trusted permit processor and sync. * * @dev Throws when caller is not the owner of the trusted permit processor list. * * @dev

Postconditions:

* @dev 1. `trustedPermitProcessor` has been approved in `_trustedPermitProcessors` list. * @dev 2. A `TrustedPermitProcessorAdded` event has been emitted. * * @param permitProcessors The addresses of trusted permit processors to add to the list. * @param paymentProcessorsToSync An array of the payment processors to sync the removal with. */ function addTrustedPermitProcessors( address[] calldata permitProcessors, address[] calldata paymentProcessorsToSync ) external; ``` ### removeTrustedPermitProcessors Callable by the owner of the default payment method whitelist to remove a permit processor instance. ```solidity /** * @notice Allows the trusted permit processor list owner to remove a trusted permit processor and sync. * * @dev Throws when caller is not the owner of the trusted permit processor list. * * @dev

Postconditions:

* @dev 1. `trustedPermitProcessor` has been removed from the `_trustedPermitProcessors` list. * @dev 2. A `TrustedPermitProcessorRemoved` event has been emitted. * * @param permitProcessors The addresses of trusted permit processors to remove from the list. * @param paymentProcessorsToSync An array of the payment processors to sync the removal with. */ function removeTrustedPermitProcessors( address[] calldata permitProcessors, address[] calldata paymentProcessorsToSync ) external; ``` ### setCollectionPaymentSettings Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function will configure the payment settings rules to apply to all orders executed on Payment Processor. ```solidity /** * @notice Allows the smart contract, the contract owner, or the contract admin of any NFT collection to * specify the payment settings for their collections. * * @dev Throws when the specified tokenAddress is address(0). * @dev Throws when the caller is not the contract, the owner or the administrator of the specified tokenAddress. * @dev Throws when the royalty backfill numerator is greater than 10,000. * @dev Throws when the royalty bounty numerator is greater than 10,000. * @dev Throws when the specified payment method whitelist id does not exist. * * @dev

Postconditions:

* @dev 1. The `PaymentSettings` type for the collection has been set. * @dev 2. The `paymentMethodWhitelistId` for the collection has been set, if applicable. * @dev 3. The `constrainedPricingPaymentMethod` for the collection has been set, if applicable. * @dev 4. The `royaltyBackfillNumerator` for the collection has been set. * @dev 5. The `royaltyBackfillReceiver` for the collection has been set. * @dev 6. The `royaltyBountyNumerator` for the collection has been set. * @dev 7. The `exclusiveBountyReceiver` for the collection has been set. * @dev 8. The `blockTradesFromUntrustedChannels` for the collection has been set. * @dev 8. An `UpdatedCollectionPaymentSettings` event has been emitted. * * @param tokenAddress The smart contract address of the NFT collection. * @param params The payment settings parameters for the collection. * @param dataExtensions The data extensions to set. * @param dataSettings The data settings to set. * @param wordExtensions The word extensions to set. * @param wordSettings The word settings to set. * @param paymentProcessorsToSync An array of the payment processors to sync the removal with. */ function setCollectionPaymentSettings( address tokenAddress, CollectionPaymentSettingsParams memory params, bytes32[] memory dataExtensions, bytes[] memory dataSettings, bytes32[] memory wordExtensions, bytes32[] memory wordSettings, address[] calldata paymentProcessorsToSync ) external; ``` ### setTokenPricingBounds Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function sets the floor and ceiling price for specific tokens ```solidity /** * @notice Allows the smart contract, the contract owner, or the contract admin of any NFT collection to * specify their own bounded price at the individual token level. * * @dev Throws when the specified tokenAddress is address(0). * @dev Throws when the caller is not the contract, the owner or the administrator of the specified tokenAddress. * @dev Throws when the lengths of the tokenIds and pricingBounds array don't match. * @dev Throws when the tokenIds or pricingBounds array length is zero. * @dev Throws when the any of the specified floor prices is greater than the ceiling price for that token id. * * @dev

Postconditions:

* @dev 1. The token-level pricing bounds for the specified tokenAddress and token ids has been set. * @dev 2. An `UpdatedTokenLevelPricingBoundaries` event has been emitted. * * @param tokenAddress The smart contract address of the NFT collection. * @param tokenIds An array of token ids for which pricing bounds are being set. * @param pricingBounds An array of pricing bounds used to set the floor and ceiling per token. * @param paymentProcessorsToSync An array of the payment processors to sync the removal with. */ function setTokenPricingBounds( address tokenAddress, uint256[] calldata tokenIds, RegistryPricingBounds[] calldata pricingBounds, address[] calldata paymentProcessorsToSync ) external; ``` ### addTrustedChannelForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function approves a series of channel to be allowed to execute orders for the collection when `blockTradesFromUntrustedChannels` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). See [Trusted Forwarder](../../../trusted-forwarder/overview) for more information on channels. ```solidity /** * @notice Allows trusted channels to be added to a collection. * * @dev Throws when the specified tokenAddress is address(0). * @dev Throws when the caller is not the contract, the owner or the administrator of the specified tokenAddress. * @dev Throws when the specified address is not a trusted forwarder. * * @dev

Postconditions:

* @dev 1. `channel` has been approved for trusted forwarding of trades on a collection. * @dev 2. A `TrustedChannelAddedForCollection` event has been emitted. * * @param tokenAddress The smart contract address of the NFT collection. * @param channels The smart contract address of the trusted channel. * @param paymentProcessorsToSync An array of the payment processors to sync the addition with. */ function addTrustedChannelForCollection( address tokenAddress, address[] calldata channels, address[] calldata paymentProcessorsToSync ) external; ``` ### removeTrustedChannelForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function removes a series of channels from the approval to execute orders for the collection when `blockTradesFromUntrustedChannels` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). See [Trusted Forwarder](../../../trusted-forwarder/overview) for more information on channels. ```solidity /** * @notice Allows trusted channels to be removed from a collection. * * @dev Throws when the specified tokenAddress is address(0). * @dev Throws when the caller is not the contract, the owner or the administrator of the specified tokenAddress. * * @dev

Postconditions:

* @dev 1. `channel` has been dis-approved for trusted forwarding of trades on a collection. * @dev 2. A `TrustedChannelRemovedForCollection` event has been emitted. * * @param tokenAddress The smart contract address of the NFT collection. * @param channels The smart contract address of the trusted channel to remove. * @param paymentProcessorsToSync The payment processors to sync with. */ function removeTrustedChannelForCollection( address tokenAddress, address[] calldata channels, address[] calldata paymentProcessorsToSync ) external; ``` ## Data Structures ### RegistryPricingBounds | isSet | floorPrice | ceilingPrice | |-------|------------|--------------| | bool | uint120 | uint120 | - isSet: Boolean true/false flag to indicate the pricing bounds have been set. Distinguishes set zero-value bounds from unset bounds that have default values of zero in the storage slot. - floorPrice: Minimum value to accept for a sales price without rejecting the sale. - ceilingPrice: Maximum value to accept for a sales price without rejecting the sale. ```solidity struct RegistryPricingBounds { bool isSet; uint120 floorPrice; uint120 ceilingPrice; } ``` ### CollectionPaymentSettingsParams |paymentSettings|paymentMethodWhitelistId|constrainedPricingPaymentMethod|royaltyBackfillNumerator|royaltyBackfillReceiver|royaltyBountyNumerator|exclusiveBountyReceiver|extraData| collectionMinimumFloorPrice|collectionMaximumCeilingPrice| |---------------|------------------------|-------------------------------|------------------------|-----------------------|----------------------|-----------------------|---------|---------------------------|-----------------------------| | uint8 | uint32 | address | uint16 | address | uint16 | address | uint16 | uint120 | uint120 | - `paymentSettings`: The payment settings for the collection. - `paymentMethodWhitelistId`: The ID of the payment method whitelist to use for the collection. - `constrainedPricingPaymentMethod`: The payment method to use for constrained pricing. - `royaltyBackfillNumerator`: The numerator to use for the royalty backfill. - `royaltyBackfillReceiver`: The address to use as the royalty backfill receiver. - `royaltyBountyNumerator`: The numerator to use for the royalty bounty. - `exclusiveBountyReceiver`: The address to use as the exclusive bounty receiver. - `extraData`: Extra data for the payment settings. The first 8 bits of this data contains the SettingsRegistry Flags. - `gasLimitOverride`: The gas limit override for the collection. - `collectionMinimumFloorPrice`: The minimum floor price for the collection. - `collectionMaximumCeilingPrice`: The maximum ceiling price for the collection. ### CollectionRegistryPaymentSettings | initialized | paymentSettingsType | paymentMethodWhitelistId | royaltyBackfillReceiver | royaltyBackfillNumerator | royaltyBountyNumerator | extraData | |-------------|---------------------|--------------------------|-------------------------|--------------------------|------------------------|-----------| | bool | uint8 | uint32 | address | uint16 | uint16 | uint16 | - `initialized`: True if the payment settings have been initialized, false otherwise. - `paymentSettingsType`: The type of payment settings for the collection. - `paymentMethodWhitelistId`: The ID of the payment method whitelist to use for the collection. - `royaltyBackfillReceiver`: The address to use as the royalty backfill receiver. - `royaltyBackfillNumerator`: The numerator to use for the royalty backfill. - `royaltyBountyNumerator`: The numerator to use for the royalty bounty. - `extraData`: Extra data for the payment settings. The first 8 bits of this data contains the SettingsRegistry Flags. ## Settings Registry Flags In Payment Processor V3.0.0, flags are used to efficiently store and retreive boolean values in a single uint8. These flags are stored in the `_collectionPaymentSettings` mapping as `CollectionRegistryPaymentSettings.extraData`. ```solidity uint8 constant FLAG_IS_ROYALTY_BOUNTY_EXCLUSIVE = 1 << 0; uint8 constant FLAG_BLOCK_TRADES_FROM_UNTRUSTED_CHANNELS = 1 << 1; uint8 constant FLAG_USE_BACKFILL_AS_ROYALTY_SOURCE = 1 << 2; ``` Flags are stored in a single uint8 which uninitialized looks like 0000000000000000. When utilizing flags, bit shifts are used to mask the value. For example, `FLAG_IS_ROYALTY_BOUNTY_EXCLUSIVE == 1 << 0` which would set the uint8 to 0000000000000001. To check the whether a flag is set, we can use bitwise AND (&). For example, if we want to check if `FLAG_IS_ROYALTY_BOUNTY_EXCLUSIVE` is set, you would use: `if (flags & FLAG_IS_ROYALTY_BOUNTY_EXCLUSIVE != 0)`. ### Exclusive Royalty Bounty Flag The exclusive royalty bounty flag is used to indicate whether an exclusive marketplace has been designated as eligible to receive a royalty bounty. If the flag is set, only the address eligible to receive a royalty bounty is the address set in `collectionExclusiveBountyReceivers[tokenAddress]`. ### Block Trades From Untrusted Channels Flag The block trades from untrested channels flag is used to indicate whether trades originating from non-permissioned channels should be reverted. If the flag is set, all trades will be checked against `collectionTrustedChannels[tokenAddress]`. If the originator is not registered as a trusted channel for the token contract address, the trade will be reverted. ### Use Backfill As Royalty Source Flag The use backfill as royalty source flag is used to indicate whether a royalty source should be backfilled. If a token contract is not ERC2981 compliant, creators can set backfilled royalty to ensure that royalties are honored on any trade flow that occurs through Payment Processor. # Glossary Here are some helpful terms to keep in mind. - **Payment Processor**: An NFT exchange protocol built by creators, for creators. Built for trading of ERC721-C and ERC1155-C tokens, but backwards compatible to support trading of any ERC721 or ERC1155 token as well. Analagous to Blur Marketplace and Seaport exchange protocols, but built entirely around honoring fully on-chain programmable royalties. Also known as `PaymentProcessor`. - **PaymentProcessor**: Shorthand for `Payment Processor`. - **Payment Processor Encoder**: A helper contract deployed alongside Payment Processor that integrated marketplaces use to format Payment Processor function calldata. - **Maker(s)**: Create buying or selling orders that are not carried out immediately. For example, "sell NFT `A` at a price of $100" or "buy NFT `B` for $100". This creates liquidity, meaning that it is easier for others to instantly buy or sell NFTs when the conditions are met. When Bob lists an NFT he owns for sale, Bob is considered maker of the order. Similarly, when Bob offers to buy an NFT, Bob is considered the maker of the order. - **Taker(s)**: The entities that buy or sell instantly are called takers. In other words, the takers fill the orders created by the makers. When Alice executes and order to buy Bob's listing, Alice is considered the taker of the order. Similarly, when Alice accepts an offer from Bob to buy an NFT she owns, Alice is considered the taker of the order. - **Order(s)**: Payment Processor supports two broad categories of orders and four types: - **Standard Order**: Any listing or offer that may be filled directly by a taker. Standard orders, when cancelled, must be cancelled on-chain by the order maker at their own gas expense. Standard orders are not susceptible to censorship provided they are accessible through an orderbook API. - **Cosigned Order**: Any listing or offer that must be cosigned by another party in order to be filled by a taker. Cosigned orders, when cancelled, can be gaslessly cancelled off-chain. Order-book providers that support cosigned orders must properly secure and automate the co-signing process. The order-book provider must guarantee that no cosignature can be generated for a previously cancelled order. Similarly, the order-book provider must guarantee availability of cosignatures to takers such that orders can always be filled uninterrupted. The use of cosigned orders SHOULD be at the order maker's discretion. Pros include cheaper, faster cancellation/replacement of orders without incurring transaction gas costs. Cons include censorship/denial of service risks. - **Listing**: An order (standard or cosigned) to sell an NFT once filled by a taker. Listings are gaslessly signed off-chain by the owner of the NFT. - **Item Offer**: An order (standard or cosigned) to buy a single, specific NFT once filled by a taker. Item offers apply to exactly one specific token id in a collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing the NFT. - **Collection Offer**: An order (standard or cosigned) to buy any NFT belonging to a specific collection once filled by a taker. Collection offers apply to any token id for a specific collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing an NFT. - **Token Set Offer**: An order (standard or cosigned) to buy a single NFT from a subset of token ids in a specific collection once filled by a taker. Token set offers apply to a subset of token ids for a specific collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing an NFT. - **Beneficiary**: The address of the account that receives the NFT/item when an order is filled. The buyer and beneficiary can be, but don't have to be the same account. Beneficiaries are acknowledged and signed in offer orders when an offer is made. Alternately, the beneficiary is designated and signed in the calldata of the buy transaction when the taker buys a listing. - **Marketplace Fee**: A fee paid to the primary marketplace where an order was made. The marketplace fee is acknowledged in signed orders. - **Fee On Top**: A `fee on top` is typically reserved for a marketplace or specialized wallet that finds orders filled by the taker. This taker marketplace fee is an optional fee paid by the taker in excess of the items' prices in one or more orders. - **Royalty Bounty**: A percentage, optionally paid out of a creator's royalty on each trade, to the marketplace where the NFT order was made. Royalty bounties are optional, and creators can configure a percentage from 0-100% to share with marketplaces that provide liquidity. Royalty bounties are a powerful tool to align incentives between creators and marketplaces. - **Bulk Orders**: A signature provided by a maker which includes multiple approvals, confirmed by signing the root of a merkle tree and executed by takers who provide merkle proofs. - **Permit Orders**: An order which utilizes `PermitC` or other similar permit processors to handle approvals for ERC20, ERC721 and ERC1155 as well as validate additional data. - **Minimum Protocol Fee**: The minimum fee rate (BPS) collected by the Payment Processor procotocol. Default is 25 BPS (0.25%) unless otherwise configured/adjusted. - **Protocol Exchange Fee Tax**: The rate at which the marketplace/exchange fee is taxed. This is a tax the marketplace pays for the right to monetize trading using the Payment Processor protocol and is used to fund ongoing protocol development and enhancements. Protocol fees collected this way may fully or partially offset the minimum protocol fee to limit the excess fee applied to the seller's proceeds. - **Protocol Fee On Top Tax**: The rate at which the fee on top is taxed. This is a tax the app injecting a fee on top pays for the right to monetize trading using the Payment Processor protocol and is used to fund ongoing protocol development and enhancements. - **Protocol Fee Version**: If the protocol fee is adjusted by the fee manager, it creates a new version. Traders must acknowledge the protocol fee version in their sale approval signatures or their signed transaction to sell an item. Protocol fee versions have a grace period so that existing orders can be filled before they become invalidated and require new orders to use the latest fee version. # Events ## **Collection Settings Registry Events** The following events are emitted by the Collection Settings Registry used by creators to configure their collection trading settings. ### ***Payment Methods and Settings Events*** ```solidity /// @notice Emitted when a new payment method whitelist is created event CreatedPaymentMethodWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed whitelistOwner, string whitelistName); /// @notice Emitted when a coin is added to the approved coins mapping for a whitelist event PaymentMethodAddedToWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); /// @notice Emitted when a coin is removed from the approved coins mapping for a whitelist event PaymentMethodRemovedFromWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); /// @notice Emitted when a payment method whitelist is reassigned to a new owner event ReassignedPaymentMethodWhitelistOwnership( uint32 indexed id, address indexed newOwner); /// @notice Emitted when payment settings are updated for a collection event UpdatedCollectionPaymentSettings( address indexed tokenAddress, CollectionPaymentSettingsParams params); /// @notice Emitted whenever pricing bounds change at a token level for price-constrained collections. event UpdatedTokenLevelPricingBoundaries( address indexed tokenAddress, uint256 indexed tokenId, uint256 floorPrice, uint256 ceilingPrice); ``` ### ***Trusted Channel Events*** ```solidity /// @notice Emitted when a trusted channel is added for a collection event TrustedChannelAddedForCollection( address indexed tokenAddress, address indexed channel); /// @notice Emitted when a trusted channel is removed for a collection event TrustedChannelRemovedForCollection( address indexed tokenAddress, address indexed channel); ``` ### ***Permit Processor Events*** ```solidity /// @notice Emitted when a permit processor is added to the trusted permit processor list. event TrustedPermitProcessorAdded(address indexed permitProcessor); /// @notice Emitted when a permit processor is removed from the trusted permit processor list. event TrustedPermitProcessorRemoved(address indexed permitProcessor); ``` ## **Payment Processor Events** Payment Processor emits several indexible events that should be indexed by off-chain order books and analytics platforms. ### ***Fill Or Kill Trade Events*** Off-chain order books and analytics platforms can track the following events for trading volume and metrics, as well as trades by user. ```solidity /// @notice Emitted when an ERC721 listing is purchased. event BuyListingERC721( address indexed buyer, address indexed seller, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 salePrice); /// @notice Emitted when an ERC1155 listing is purchased. event BuyListingERC1155( address indexed buyer, address indexed seller, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 amount, uint256 salePrice); /// @notice Emitted when an ERC721 offer is accepted. event AcceptOfferERC721( address indexed seller, address indexed buyer, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 salePrice); /// @notice Emitted when an ERC1155 offer is accepted. event AcceptOfferERC1155( address indexed seller, address indexed buyer, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 amount, uint256 salePrice); ``` ### ***Partial Fill Trade Events*** ```solidity /// @notice Emitted the first time a partially fillable 1155 order has items filled on-chain. event OrderDigestOpened( bytes32 indexed orderDigest, address indexed account, uint256 orderStartAmount); /// @notice Emitted when a user fills items on a partially fillable 1155 listing. event OrderDigestItemsFilled( bytes32 indexed orderDigest, address indexed account, uint256 amountFilled); ``` ### ***Order Cancellation Events*** Off-chain order books SHOULD index the following events to track what orders have been filled or cancelled and which nonces are un-available. ```solidity /// @notice Emitted when a user revokes all of their existing listings or offers that share the master nonce. event MasterNonceInvalidated( uint256 indexed nonce, address indexed account); /// @notice Emitted when a user revokes a single listing or offer nonce for a specific marketplace, or the order /// filled successfully. event NonceInvalidated( uint256 indexed nonce, address indexed account, bool wasCancellation); /// @notice Emitted when a user fills a single listing or offer nonce but item fails to transfer/fill. event NonceRestored( uint256 indexed nonce, address indexed account); /// @notice Emitted when a user revokes a single permitted listing nonce, or the order filled successfully. event PermittedOrderNonceInvalidated( uint256 indexed permitNonce, uint256 indexed orderNonce, address indexed account, bool wasCancellation); /// @notice Emitted when a user revokes a single partially fillable 1155 listing or offer for a specific marketplace, /// or a partially fillable order has been completely filled. event OrderDigestInvalidated( bytes32 indexed orderDigest, address indexed account, bool wasCancellation); /// @notice Emitted when a user fills a partially fillable 1155 listing or offer but item fails to transfer/fill. event OrderDigestItemsRestored( bytes32 indexed orderDigest, address indexed account, uint256 amountRestoredToOrder); ``` ### ***Cosigner Events*** Off-chain order books SHOULD index the following event to track when a cosigner has been destroyed and filter any open orders that require a cosignature from the cosigner. ```solidity /// @notice Emitted when a cosigner destroys itself. event DestroyedCosigner(address indexed cosigner); ``` ### ***Collection Payment Settings Events*** Off-chain order books SHOULD index collection payment settings events to track allowed payment methods, backfilled royalties, and royalty bounties. ```solidity /// @notice Emitted when payment settings for a collection are updated. event UpdatedCollectionPaymentSettings( address indexed tokenAddress, PaymentSettings paymentSettings, uint32 indexed paymentMethodWhitelistId, address indexed constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool useRoyaltyBackfillAsRoyaltySource); ``` ### ***Custom Payment Method Whitelist Events*** Off-chain order books SHOULD index payment method whitelists by id. By doing so, when a collection applies a custom payment method whitelist id, exchanges can be informed about which payment methods are allowed for a give collection. ```solidity /// @notice Emitted when a coin is added to the approved coins mapping for a whitelist event PaymentMethodAddedToWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); /// @notice Emitted when a coin is removed from the approved coins mapping for a whitelist event PaymentMethodRemovedFromWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); ``` ### ***Bounded Pricing Events*** Off-chain order books SHOULD index pricing bounds events to track min and max allowable sale prices for collections that implement pricing constraints. ```solidity /// @notice Emitted whenever pricing bounds change at a collection level for price-constrained collections. event UpdatedCollectionLevelPricingBoundaries( address indexed tokenAddress, uint256 floorPrice, uint256 ceilingPrice); /// @notice Emitted whenever pricing bounds change at a token level for price-constrained collections. event UpdatedTokenLevelPricingBoundaries( address indexed tokenAddress, uint256 indexed tokenId, uint256 floorPrice, uint256 ceilingPrice); ``` ### ***Trusted Channel Events*** Off-chain order books SHOULD index trusted channel events to be used in conjunction with the [`Collection Payment Settings Events`](#collection-payment-settings-events) to determine the allowed channels for a collection if `blockTradesFromUntrustedChannels` is set to `true`. ```solidity /// @notice Emitted when a trusted channel is added for a collection event TrustedChannelAddedForCollection( address indexed tokenAddress, address indexed channel); /// @notice Emitted when a trusted channel is removed for a collection event TrustedChannelRemovedForCollection( address indexed tokenAddress, address indexed channel); ``` ### ***Permit Processor Events*** ```solidity /// @notice Emitted when a permit processor is added to the trusted permit processor list. event TrustedPermitProcessorAdded(address indexed permitProcessor); /// @notice Emitted when a permit processor is removed from the trusted permit processor list. event TrustedPermitProcessorRemoved(address indexed permitProcessor); ``` ### ***Protocol Fee Events*** ```solidity /// @notice Emitted when protocol fees are updated. event ProtocolFeesUpdated( address indexed newProtocolFeeRecipient, uint16 minimumProtocolFeeBps, uint16 marketplaceFeeProtocolTaxBps, uint16 feeOnTopProtocolTaxBps, uint48 gracePeriodExpiration); ``` # Data Structures The following subsections detail the data structures needed for the creation of and filling of orders in Payment Processor. Note that maker signature formats vary by order-type. ## ***SignatureECDSA*** | V | R | S | |---------|---------|---------| | uint256 | bytes32 | bytes32 | - V: The `v` component of an ECDSA signature. - R: The `r` component of an ECDSA signature. - S: The `s` component of an ECDSA signature. *[Note: For a detailed explanation of ECDSA signatures read this article](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7).* ## ***Cosignature*** | Signer | Taker | Expiration | V | R | S | |---------|---------|------------|---------|---------|---------| | address | address | uint256 | uint256 | bytes32 | bytes32 | - Signer: The co-signer designated and acknowledged in the order maker's signature approving the order. MUST be an EOA, or `address(0)` when the co-signature is empty. It is expected that marketplaces implementing co-signing MUST expose an API enabling the taker to retrieve a co-signature approving the fill of co-signed orders. - Taker: The address of the account filling the co-signed order. May be an EOA or Smart Contract account, or `address(0)` when the co-signature is empty. A marketplace's co-signing API service MUST acknowledge the taker address in the co-signature. - Expiration: The unix timestamp (in seconds) when the co-signature expires, or `0` when the co-signature is empty. A marketplace's co-signing API should use the current unix timestamp and add a validity time that is acknowledged in the co-signature. If the validity time is too short, there might not be sufficient time for the fill transaction to be confirmed on-chain, possibly resulting in failed fill transactions. However, the validity time should also not be too long, as a taker could sit on a fill transaction for quite a while. It is left up to the marketplace to determine an appropriate validity time. - V: The `v` component of the co-signer's signature. - R: The `r` component of the co-signer's signature. - S: The `s` component of the co-signer's signature. *Note: The co-signature is generated when the co-signer creates an ECDSA signature of the Cosignature typed data message.* ## ***FeeOnTop*** A `fee on top` is typically reserved for a marketplace or specialized wallet that found the order filled by the taker. This taker marketplace fee is an optional fee paid by the taker in excess of the items' prices in one or more orders. When the maker and taker marketplace is the same, it is strongly encouraged not to apply this fee, as the fee can already be assessed in the maker fee. Note that the `fee on top` is paid in the same currency as the order's payment method. | Recipient | Amount | |-----------|---------| | address | uint256 | - Recipient: The address to which the fee is paid, or `address(0)` when the fee is empty. - Amount: The absolute amount of the fee, in wei, or `0` when the fee is empty. Note that the fee amount is not permitted to exceed the item sale price of any given order. ## ***TokenSetProof*** Token set proofs are required to fill token set offers on a collection. A token set offer is a merkle tree where the leaf data is the keccak256 hash of the collection address and token id. ``` leafHash = keccak256(abi.encode(collectionAddress, tokenId)); ``` | Proof | |-----------| | bytes32[] | - Proof: The merkle proof for the specific collection/token id leaf node being filled, , or `bytes32[](0)` when the order being filled is not a token set offer. ## ***Order*** This data structure is used to fill all single and bulk orders that are not sweep orders. | Protocol | Maker | Beneficiary | Marketplace | Fallback Royalty Recipient | Payment Method | Token Address | Token Id | Amount | Item Price | Nonce | Expiration | Marketplace Fee Numerator | Max Royalty Fee Numerator | Requested Fill Amount | Minimum Fill Amount | Protocol Fee Version | |----------|---------|-------------|-------------|----------------------------|----------------|---------------|----------|---------|------------|---------|------------|---------------------------|---------------------------|-----------------------|---------------------|--------| |uint256 | address | address | address | address | address | address | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | - Protocol: `0` for `ERC721_FILL_OR_KILL`, `1` for `ERC1155_FILL_OR_KILL`, or `2` for `ERC1155_FILL_PARTIAL` collections. `FILL_PARTIAL` means an order is partially fillable across multiple trades, while `FILL_OR_KILL` means the entire amount specified in the order must be filled in a single transaction. - Maker: The address of the account the created the order. May be an EOA or Smart Contract account. When the order was a listing, the maker is the seller of the item. When the order was an offer, the maker is the buyer of the item. - Beneficiary: The address of the account that receives the item when the order is filled. When the order was a listing, the taker (`msg.sender`) can either buy with themselves as the beneficiary, or another account. When the order was an offer, the maker would be the buyer, and the beneficiary address can either be the maker's account or another account. For example, the buyer could be a user's hot wallet, and the beneficiary could be the user's cold storage wallet. - Marketplace: The address to which the primary (maker) marketplace fee should be paid, or `address(0)` if the maker marketplace charges no platform fees. Note that for collections that offer non-exclusive royalty bounties, the maker marketplace also receives a royalty bounty paid out of creator royalties. For collections that offer exclusive royalty bounties, the maker marketplace receives a royalty bounty only if it matches the exclusive royalty bounty recipient designated by the collection creator. - Fallback Royalty Recipient: Used to allow marketplaces to support royalties for collections that do not implement ERC-2981, do not have a contract owner and do not have a user with default admin role. Fallback royalties are calculated using the `Max Royalty Fee Numerator` for the order. - Payment Method: The address of the ERC-20 coin used to fill the trade, or `address(0)` for the native currency of the chain the trade executed on. For example, `address(0)` denotes ETH on ETH Mainnet, and Matic on Polygon Mainnet. - Token Address: The address of the collection. - Token Id: The id of the token (if collection is ERC721), or the token type id of the token (if collection is ERC1155). - Amount: The number of tokens. MUST be `1` if collection is ERC721, and MUST be greater than or equal to `1` if collection is ERC1155. - Item Price: The price of the token(s) in the order. Item price may not exceed type(uint240).max. - Nonce: A unique identifier to the order that prevents replay attacks. Nonce applies only to standard signed orders and may not be re-used. For co-signed order, nonce should be set to `0`. Note: It is easier to generate a random nonce than attempt to keep track of nonces that have been used. However, be aware that a gas optimization is in place such that filled or cancelled nonces are tracked in a bitmap. Nonces 0-255, 256-511, 512-767, etc are stored in the same slot. Each maker address has its own set of nonces generated for gasless listings such that `nonce 1234` is stored separately for makers `address(5678)` and `address(abcd)` but storage of the same nonce for the same maker is common across all marketplaces. It is possible that marketplaces will have no knowledge of nonces used in outstanding order signatures. Marketplaces should employ a nonce sequencing methodology that reduces the probability of overlapping nonce usage with other marketplaces. Examples of this could be a fixed set of upper bits for all orders placed through that marketplace where nonces `0x12340000...0000` to `0x1234FFFF...FFFF` relate to a specific marketplace or the development of a common nonce issuance service that tracks all nonces used by a maker. - Expiration: The unix timestamp (in seconds) when the maker's order signature expires. A marketplace's order making API should use the current unix timestamp and add a user-defined validity time that is acknowledged in the maker's signature. - Marketplace Fee Numerator: Marketplace fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. - Max Royalty Fee Numerator: Maximum approved royalty fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. When requesting the order signature from the order maker, the marketplace MUST first attempt to read the royalties for the individual token using the EIP-2981 `royaltyInfo` function call on the collection. If `royaltyInfo` raises an exception (likely because it is unimplemented), the marketplace MUST attempt to determine if royalties have been backfilled by calling the `collectionRoyaltyBackfillSettings` function on Payment Processor. If no on-chain royalties are present, this may be set to `0`. - Requested/Minimum Fill Amount: When a fill transaction is submitted, this is the number of items the taker wishes to fill. If insufficient items remain from the partially filled order, the remaining number of items is filled instead, provided the number of remaining items its greater than or equal to the Minimum Fill Amount. - Protocol Fee Version: The protocol fee version to use when applying protocol fees. ## ***Sweep Order*** This data structure is used to fill sweep orders, and represents the shared values that apply to all order in the sweep. | Protocol | Token Address | Payment Method | Beneficiary | |----------| --------------|----------------|-------------| | uint256 | address | address | address | - Protocol: `0` for ERC721 or `1` for ERC1155 collections. - Token Address: The address of the collection. - Payment Method: The address of the ERC-20 coin used to fill the trade, or `address(0)` for the native currency of the chain the trade executed on. For example, `address(0)` denotes ETH on ETH Mainnet, and Matic on Polygon Mainnet. - Beneficiary: The address of the account that receives the item when the order is filled. ## ***Sweep Item*** This data structure is used to fill sweep orders, and represents the values that apply to individual orders in the sweep. | Maker | Marketplace | Fallback Royalty Recipient | Token Id | Amount | Item Price | Nonce | Expiration | Marketplace Fee Numerator | Max Royalty Fee Numerator | Protocol Fee Version | |---------|-------------|----------------------------|----------|------------|------------|---------|------------|---------------------------|---------------------------|---------------------------| | address | address | address | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | uint256 | - Maker: The address of the account the created the order. May be an EOA or Smart Contract account. When the order was a listing, the maker is the seller of the item. When the order was an offer, the maker is the buyer of the item. - Marketplace: The address to which the primary (maker) marketplace fee should be paid, or `address(0)` if the maker marketplace charges no platform fees. Note that for collections that offer non-exclusive royalty bounties, the maker marketplace also receives a royalty bounty paid out of creator royalties. For collections that offer exclusive royalty bounties, the maker marketplace receives a royalty bounty only if it matches the exclusive royalty bounty recipient designated by the collection creator. - Fallback Royalty Recipient: Used to allow marketplaces to support royalties for collections that do not implement ERC-2981, do not have a contract owner and do not have a user with default admin role. Fallback royalties are calculated using the `Max Royalty Fee Numerator` for the sweep item. - Token Id: The id of the token (if collection is ERC721), or the token type id of the token (if collection is ERC1155). - Amount: The number of tokens. MUST be `1` if collection is ERC721, and MUST be greater than or equal to `1` if collection is ERC1155. - Item Price: The price of the token(s) in the order. May not exceed type(uint240).max. - Nonce: A unique identifier to the order that prevents replay attacks. Nonce applies only to standard signed orders and may not be re-used. For co-signed order, nonce should be set to `0`. Note: It is easier to generate a random nonce than attempt to keep track of nonces that have been used. However, be aware that a gas optimization is in place such that filled or cancelled nonces are tracked in a bitmap. Nonces 0-255, 256-511, 512-767, etc are stored in the same slot. Each maker address has its own set of nonces generated for gasless listings such that `nonce 1234` is stored separately for makers `address(5678)` and `address(abcd)` but storage of the same nonce for the same maker is common across all marketplaces. It is possible that marketplaces will have no knowledge of nonces used in outstanding order signatures. Marketplaces should employ a nonce sequencing methodology that reduces the probability of overlapping nonce usage with other marketplaces. Examples of this could be a fixed set of upper bits for all orders placed through that marketplace where nonces `0x12340000...0000` to `0x1234FFFF...FFFF` relate to a specific marketplace or the development of a common nonce issuance service that tracks all nonces used by a maker. - Expiration: The unix timestamp (in seconds) when the maker's order signature expires. A marketplace's order making API should use the current unix timestamp and add a user-defined validity time that is acknowledged in the maker's signature. - Marketplace Fee Numerator: Marketplace fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. - Max Royalty Fee Numerator: Maximum approved royalty fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. When requesting the order signature from the order maker, the marketplace MUST first attempt to read the royalties for the individual token using the EIP-2981 `royaltyInfo` function call on the collection. If `royaltyInfo` raises an exception (likely because it is unimplemented), the marketplace MUST attempt to determine if royalties have been backfilled by calling the `collectionRoyaltyBackfillSettings` function on Payment Processor. If no on-chain royalties are present, this may be set to `0`. - Protocol Fee Version: The protocol fee version to use when applying protocol fees. ## ***Bulk Order Proof*** | orderIndex | proof | |------------|-----------| | uint256 | bytes32[] | - Order Index: The index of the order within the structure of the merkle tree. This is used to determine what side of the merkle tree the order falls on and used in conjunction with the provided merkle proof to recreate the root. - Proof: An array of bytes32 hashes which are used to complete the merkle tree and generate the root along with the order digest. ## ***Permit Context*** | permitProcessor | permitNonce | |-----------------|-------------| | address | uint256 | - Permit Processor: The address of the Permit Processor contract. In order to be compatible with the Payment Processor, the provided contract must implement the `_permitTransferFromWithAdditionalData` variants in `PermitC` as well as the `fillPermittedOrderERC1155` function. - Permit Nonce: The nonce of the permit processor to use to ensure that the transaction is only executed once. After a nonce is invalidated, the transaction cannot be replayed. ## ***Advanced Order*** This struct defines the items required to execute an advanced order. | saleDetails | signature | cosignature | permitContext | |-------------|----------------|-------------|---------------| | Order | SignatureECDSA | Cosignature | PermitContext | - saleDetails: The order execution parameters. - signature: The signature of the maker authorizing order execution. - cosignature: The cosignature of the maker authorizing the order execution (when cosigning enabled for an order). - permitContext: Contains the address of the permit processor and the permit nonce to be used. ## ***Advanced Bid Order*** This struct defines the items required to execute an advanced bid order. | offerType | advancedOrder | sellerPermitSignature | |-----------|---------------|-----------------------| | uint256 | AdvancedOrder | SignatureECDSA | - offerType: The type of offer to execute [(0) Offer for any item in a collection. (1) Offer for a specific item in a collection. (2) Offer for a set of tokens in a collection.] - advancedOrder: The order execution parameters. - sellerPermitSignature: The signature of the seller to be used for the permit. ## ***Advanced Sweep*** This struct defines the items required to execute an advanced sweep order. | feeOnTop | sweepOrder | items | |----------|------------|---------------------| | FeeOnTop | SweepOrder | AdvancedSweepItem[] | - feeOnTop: The additional fee to be paid by the taker. - sweepOrder: The order execution parameters. - items: An array of items to be executed as part of the sweep order. ## ***Advanced Sweep Item*** This struct is a wrapper for a sweep order item that includes the permit context, bulk order information, signature and cosignature. | sweepItem | signature | cosignature | permitContext | bulkOrderProof | |-----------|----------------|-------------|---------------|----------------| | SweepItem | SignatureECDSA | Cosignature | PermitContext | BulkOrderProof | - sweepItem: The sweep order item to be executed. - signature: The signature of the maker authorizing the order execution. - cosignature: The cosignature of the maker authorizing the order execution. - permitContext: Contains the address of the permit processor and the permit nonce to be used. - bulkOrderProof: The proof data for the bulk order. # Listings A listing is an order (standard or cosigned) to sell an NFT once filled by a taker. Listings are gaslessly signed off-chain by the owner/seller of the NFT. ## Notional Exchange Listing Flow 1. Maker browses their items and selects an item to list for sale. 2. Maker inputs: Listing Currency/Payment Method, Price, Validity Time. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage and on-chain royalty fee (display sale price, fees, and net seller proceeds) 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 5. Signature Prompt for [SaleApproval](#sale-approval) signature. 6. Order and signature submitted to order book. ## Basic Sale Approval Signatures When listing an individual NFT for sale the seller/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the order is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```solidity SaleApproval( uint8 protocol, address cosigner, address seller, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 maxRoyaltyFeeNumerator, uint256 nonce, uint256 masterNonce, uint256 protocolFeeVersion ) ``` Typed data hash: `0x36e5aab0dde52c07206863cf8fe7b4b1a25be63a3ae5286f8fe80d159c154c4f` ## Advanced Sale Approval Signatures There are two types of advanced sale approvals: permits and bulk orders. ### Permitted Sale Approval Signatures Instead of approving Payment Processor to transfer tokens, users can instead authorize Permit-C to transfer tokens and issue gasless permits allowing Payment Processor to transfer their NFTs instead. A permitted sale approval is a Permit-C transfer permit with the sale approval embedded and signed as a signle signature. The structure of the EIP-712 message for permitted sale approvals is shown below. ```solidity PermitTransferFromWithAdditionalData( uint256 tokenType, address token, uint256 id, uint256 amount, uint256 nonce, address operator, uint256 expiration, uint256 masterNonce, SaleApproval approval ) SaleApproval( uint8 protocol, address cosigner, address seller, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 maxRoyaltyFeeNumerator, uint256 nonce, uint256 masterNonce, uint256 protocolFeeVersion ) ``` Typed data hash: `0xb1cb65a3e643070d9d33047fdfd384badc7cd70b2415e548b37db5a10c708716` ### Bulk Sale Approval Signatures For users seeking to list many items at once, users can sign a bulk sale approval order. Listings signed in bulk can be filled independently of the others. Up to 1024 items may be listed with one signature. Due to how `EIP-712` struct hashing works, a nested array of `[2] * treeHeight` will produce the same hash as the root hash, provided it contains the same data and is padded with empty structs. Trees of depths 1 (`2^1 = 2`) to 10 (`2^10 = 1024`) are supported on Payment Processor. ### Bulk Order Typehashes The Bulk Order Typehash Lookup tables are generated by concatenating the hashes from each height into one bytes array. This is decoded by taking the bytes32 output of the offset of `height - 1 * 32`. ```solidity // keccack256("BulkSaleApproval(SaleApproval[2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_1_HASH = 0x8659f55d4c88f84030fe09241169834754ebf2e61099b616e530c6b65712c644; // keccack256("BulkSaleApproval(SaleApproval[2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_2_HASH = 0x5106f4043cda72e04ca2c760016628ea8d5667309323ba590682ea5254b4e82e; // keccack256("BulkSaleApproval(SaleApproval[2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_3_HASH = 0x71267a8f42e7ccde4ac75848a93b741df7a2f7a58e40274055f2fa51dbaa69ce; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_4_HASH = 0x6e90557aed2a349f8b6efb2b652c4025c56cfa82424c5cd0c6801cc234ebf519; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_5_HASH = 0xcaf42fc49ad57705ea6be2ca9e1835c9ccddf7bc6fdea8f851917329b1d53a7f; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_6_HASH = 0x09b32b6b5ffee898d5fe18398652042cc8c46449329c5a1a79e425f4ede9bc2b; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_7_HASH = 0xc2362dea3338b88607906397acfdcc5651f19b40a038ee7c89dc4ab54c736cac; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_8_HASH = 0xf2bbc4f6d7ccfa2b8aab6505f0c8f2868c5a190c6c4cfce8c0de447804904df9; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_9_HASH = 0x81f99e15003039550d3597e34e6426b1e230992a5a3727cb5e230754f18f3566; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_10_HASH = 0x012178d0a6b8f8748e48411572bdc391435539737aafc91fcba6f2c24ae156cf; ``` For more details on bulk order signing, see the [Bulk Orders section of the docs](./bulk-orders.md). # Offers Offers are gaslessly signed off-chain by one or more parties interested in purchasing the NFT. Payment Processor support three offer types: - **Item Offer**: An order (standard or cosigned) to buy a single, specific NFT once filled by a taker. Item offers apply to exactly one specific token id in a collection. - **Collection Offer**: An order (standard or cosigned) to buy any NFT belonging to a specific collection once filled by a taker. Collection offers apply to any token id for a specific collection. - **Token Set Offer**: An order (standard or cosigned) to buy a single NFT from a subset of token ids in a specific collection once filled by a taker. Token set offers apply to a subset of token ids for a specific collection. ## Notional Exchange Offer Flow ### Item Offer 1. Maker browses a collection and selects a single item to make an offer on. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds) 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). For standard orders, a nonce is generated. 5. Signature Prompt for either [ItemOfferApproval](#item-offer-approval). 6. Order and signature submitted to order book. ### Collection Offer 1. Maker browses a collection and chooses to make a collection offer for any item in the collection. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds). 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 5. Signature Prompt for either [CollectionOfferApproval](#collection-offer-approval). 6. Order and signature submitted to order book. ### Token Set Collection Offer This can take a few forms depending on how the marketplace chooses to implement this. This can take the form of offers on traits, offers on tokens that have not been stolen, etc. 1. Maker browses a collection and chooses some criteria for token ids to make an offer on. The marketplace generates a merkle tree of tokens based on the criteria. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds). 4. Maker prompted to acknowledge the set of tokens/merkle root. 5. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 6. Signature Prompt for either [TokenSetOfferApproval](#token-set-offer-approval). 7. Order and signature submitted to order book. ## Item Offer Approval When making an offer on a specific NFT the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js ItemOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce ) ``` Typed data hash: `0xce2e9706d63e89ddf7ee16ce0508a1c3c9bd1904c582db2e647e6f4690a0bf6b` ## Collection Offer Approval When making an offer on any NFT in a specific collection the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js CollectionOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce ) ``` Typed data hash: `0x8fe9498e93fe26b30ebf76fac07bd4705201c8609227362697082288e3b4af9c` ## Token Set Offer Approval When making an offer on a subset of NFTs in a specific collection the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js TokenSetOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce, bytes32 tokenSetMerkleRoot ) ``` Typed data hash: `0x244905ade6b0e455d12fb539a4b17d7f675db14797d514168d09814a09c70e70` # Filling Orders ## Filling A Single Listing (Buy Now) 1. Taker browses listings and chooses a listed item. 2. Taker selects "Buy Now". 3. Taker selects beneficiary (whether self or other account). 4. Taker prompted to review the details, including review and acknowledgement of fee on top when applicable. 5. If listing order was co-signed, client requests a co-signature for the listing. 6. Marketplace calls the PaymentProcessorEncoder `encodeBuyListingCalldata` function to generate the calldata to fill the order. [Encoding Details](./calldata-encoding#encodebuylistingcalldata) 7. Wallet pops up a transaction confirmation of the `buyListing` call. [Usage Details](#buy-listing) 8. Taker confirms the transaction through their wallet interface. ## Filling A Batch Of Listings (Shopping Cart) 1. Taker browses listings and chooses several listed items (they may be from one or more collections). 2. Taker selects "Add To Shopping Cart" for each item. 3. Taker reviews cart and "Checks Out". 4. Taker selects beneficiary (whether self or other account). 5. Taker prompted to review the details, including review and acknowledgement of all fees on top when applicable. 6. For any co-signed listing orders, client requests co-signatures. 7. Marketplace calls the PaymentProcessorEncoder `encodeBulkBuyListingsCalldata` function to generate the calldata to bulk fill the orders. [Encoding Details](./calldata-encoding#encodebulkbuylistingscalldata) 8. Wallet pops up a transaction confirmation of the `bulkBuyListings` call. [Usage Details](#bulk-buy-listings) 9. Taker confirms the transaction through their wallet interface. ## Filling A Batch of Listings (Collection Sweep) 1. Taker chooses "Sweep Collection" for the desired collection. 2. Taker specifies desired quantity and maximum price per item. 3. Taker selects beneficiary (whether self or other account). 4. Order book supplies listings that are the best matches. 5. Taker prompted to review the details, including review and acknowledgement of the fee on top when applicable. 6. For any co-signed listing orders, client requests co-signatures. 7. Marketplace calls the PaymentProcessorEncoder `encodeSweepCollectionCalldata` function to generate the calldata to fill the sweep. [Encoding Details](./calldata-encoding#encodesweepcollectioncalldata) 8. Wallet pops up a transaction confirmation of the `sweepCollection` call. [Usage Details](#sweep-collection) 9. Taker confirms the transaction through their wallet interface. ## Filling A Single Offer (Accept Offer) 1. Taker browser offers where the offer criteria matches items they own and chooses one. 2. Taker selects "Accept Offer". 3. Taker prompted to review the details, including review and acknowledgement of the current on-chain royalty fee, maker marketplace fee, and fee on top when applicable. 4. If the offer order was co-signed, client requests a co-signature for the offer. 5. Marketplace calls the PaymentProcessorEncoder `encodeAcceptOfferCalldata` function to generate the calldata to fill the order. [Encoding Details](./calldata-encoding#encodeacceptoffercalldata) 6. Wallet pops up a transaction confirmation of the `acceptOffer` call. [Usage Details](#accept-offer) 7. Taker confirms the transaction through their wallet interface. ## Filling A Batch Of Offers (Offer Cart) 1. Taker browser offers where the offer criteria matches items they own and chooses several. 2. Taker selects "Add To Offer Cart" for each item. 3. Taker reviews cart and "Checks Out". 4. Taker prompted to review the details, including review and acknowledgement of the current on-chain royalty fees for each item, the maker marketplace fees for each item, and the fees on top when applicable. 5. For any co-signed offer orders, client requests co-signatures. 6. Marketplace calls the PaymentProcessorEncoder `encodeBulkAcceptOffersCalldata` function to generate the calldata to bulk fill the orders. [Encoding Details](./calldata-encoding#encodebulkacceptofferscalldata) 7. Wallet pops up a transaction confirmation of the `bulkAcceptOffers` call. [Usage Details](#bulk-accept-offers) 8. Taker confirms the transaction through their wallet interface. ***Note: There are other steps marketplaces may need to implement to prompt users in the workflow. For instance: approving Payment Processor to transfer NFTs and ERC-20 payments, prompting to wrap native currency or swap currencies when needed, or prompting buyers to perform a one-time signature to prove they are EOAs [for select ERC721-C security levels only].*** ## Taker Operations / Functions ### Buy Listing Exchanges should call `buyListing` when a taker wants to purchase a single listing using a UX analogous to "Buy Now". There are four order types that may be filled through the `buyListing` function: **Standard Sell Order *Without* Fee On Top** - To be used when the taker fills a standard sell order and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `cosignature` and `feeOnTop` struct parameters passed to PaymentProcessorEncoder would be filled with zero values. **Standard Sell Order *With* Fee On Top** - To be used when the taker fills a standard sell order via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution the `cosignature` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `feeOnTop` struct parameter would contain the `recipient` and `amount` for the fee being added by the taker marketplace. **Cosigned Sell Order *Without* Fee On Top** - To be used when the taker fills a co-signed sell order and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `feeOnTop` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `cosignature` struct parameter would contain the `signer`, `taker`, `expiration` and cosignature `v`, `r` and `s` values for the cosignature. **Cosigned Sell Order *With* Fee On Top** - To be used when the taker fills a co-signed sell order via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution both the `feeOnTop` and `cosignature` struct parameters passed to PaymentProcessorEncoder would contain the values for the additional fee on top and cosignature validation. Use the PaymentProcessorEncoder to encode the calldata(./calldata-encoding#encodebuylistingcalldata). *Note: The taker/buyer (`msg.sender`) pays for the item and any fee on top when applicable. Because the `buyListing` function is payable, both native or ERC-20 payment methods are accepted.* ### Advanced Buy Listing Advanced orders include 2 additional features, Permits and executing listings / offers which were generated via the `Bulk Order` merkle root. These orders can include any combination of the above 4 types, but are separated to prevent extra gas costs for standard orders. **Permit Order** - To be used when the maker generated the signature including the Permit Processor field. This will include the Permit Processor required data as well as advanced data which will need to match the order that is being fulfilled. **Bulk Order** - To be used when the maker generated the signature via signing a merkle tree. The `BulkOrderProof` must include the correct merkle proof and the correct index of the listing in order to process correctly. ### Accept Offer Exchanges should call `acceptOffer` when a taker wants to sell a single item that matches an offer made by a prospective buyer. The kinds of offers are currently supported by Payment Processor: 1. Item Offer - An offer made on a specific collection where only one specific token id can be used to fill the order. 2. Collection Offer - An offer made on a specific collection where any token id can be used to fill the order. 3. Token Set Offer - An offer made on a specific collection where any token id contained in a specified subset of token ids can be used to fill the order. There are four order types that may be filled through the `acceptOffer` function: **Standard Offer *Without* Fee On Top** - To be used when the taker fills a standard buy order (offer) and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `cosignature` and `feeOnTop` struct parameters passed to PaymentProcessorEncoder would be filled with zero values. **Standard Offer *With* Fee On Top** - To be used when the taker fills a standard buy order (offer) via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution the `cosignature` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `feeOnTop` struct parameter would contain the `recipient` and `amount` for the fee being added by the taker marketplace. **Cosigned Offer *Without* Fee On Top** - To be used when the taker fills a co-signed buy order (offer) and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `feeOnTop` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `cosignature` struct parameter would contain the `signer`, `taker`, `expiration` and cosignature `v`, `r` and `s` values for the cosignature. **Cosigned Offer *With* Fee On Top** - To be used when the taker fills a co-signed buy order (offer) via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution both the `feeOnTop` and `cosignature` struct parameters passed to PaymentProcessorEncoder would contain the values for the additional fee on top and cosignature validation. *Note: The taker/seller (`msg.sender`) pays for the fee on top when applicable. The maker/buyer pays the cost of the filled item. Because the `acceptOffer` function is not payable, only ERC-20 payment methods are accepted.* ### Bulk Buy Listings Exchanges should call `bulkBuyListings` when a taker wants to purchase more than one listing using a UX analagous to a "Shopping Cart". This allows a taker to select many NFTs across different collections or with varying payment methods and fill all of the listings at once. Each listing being purchased may be any of the four transaction types supported by the `buyListing` function (see [Buy Listing](#buy-listing) above). When encoding bulk purchases the `Order`, `SignatureECDSA`, `Cosignature` and `FeeOnTop` struct values are passed as arrays of values where the item at the same index in each array corresponds to the listing being purchased. All four arrays **MUST** be of equal length and follow the same structure as a single listing purchase for standard versus cosigned orders and execution with or without a fee on top. ### Bulk Accept Offers Exchanges should call `bulkAcceptOffers` when a taker wants to accept/fill more than one offer at once. This allows a taker to sell multiple items they own across one or more collections in a single transaction. The kinds of offers are currently supported by Payment Processor: 1. Item Offer - An offer made on a specific collection where only one specific token id can be used to fill the order. 2. Collection Offer - An offer made on a specific collection where any token id can be used to fill the order. 3. Token Set Offer - An offer made on a specific collection where any token id contained in a specified subset of token ids can be used to fill the order. Each offer being accepted may be any of the four transaction types supported by the `acceptOffer` function (see [Accept Offer](#accept-offer) above). When encoding bulk accept offers the `offerType` enumerated value and `Order`, `SignatureECDSA`, `TokenSetProof`, `Cosignature` and `FeeOnTop` struct values are passed as arrays of values where the item at the same index in each array corresponds to the offer being accepted. All six arrays **MUST** be of equal length and follow the same structure as a single offer acceptance for standard versus cosigned orders and execution with or without a fee on top. ### Sweep Collection Exchanges should call `sweepCollection` (a more gas efficient form of `bulkBuyListings`) when a taker wants to purchase more than similar listings. A common UX is the "Collection Sweep" UX where the taker specifies a number of items from a collection they want to buy, with some pricing limits. For a sweep to be filled, all items in the sweep order must share the following commonalities: 1. All sell orders fillable in the sweep must be from the same collection. 3. All sell orders fillable in the sweep must use the same method of payment. 5. All sell orders fillable in the sweep must specify the same beneficiary of the NFT. When filling sweep orders, the following fields can be different for each filled item: 1. Maker 2. Marketplace 3. Fallback Royalty Recipient 4. Token Id 5. Amount 6. Item Price 7. Nonce (for standard orders) 8. Expiration 9. Marketplace Fee Numerator 10. Max Royalty Fee Numerator Sweep orders may be executed *with* or *without* a single fee on top for the sweep. To execute *with* a fee on top the `feeOnTop` struct value passed to PaymentProcessorEncoder would contain the `recipient` and `amount` for the fee. To execute *without* a fee on top the `feeOnTop` struct value passed to PaymentProcessorEncoder would contain zero values for `recipient` and `amount`. When encoding sweep orders with PaymentProcessorEncoder the `SweepItem`, `SignatureECDSA` and `Cosignature` structs are passed as arrays of values where the item at the same index in each array corresponds to the listing being purchased. All three arrays **MUST** be of equal length and follow the same structure as a single listing purchase for standard versus cosigned orders. *Note: The taker/buyer (`msg.sender`) pays for the items and any fee on top when applicable. Because the `sweepCollection` function is payable, both native or ERC-20 payment methods are accepted.* *Note: For the most gas-efficient collection sweeps, marketplaces should make a best effort to group orders in the array by marketplace address, seller, and royalty recipient.* ### Cosignature Format All cosigned listings and offers require a secondary signature to be provided at fill-time/execution time. The same cosignature format applies to all cosigned maker signatures, and takes a cosigner expiration and the maker's signature v, r, s components as typed data inputs. Note: for security purposes, co-signatures must always be signed by EOA co-signers. Furthermore, cosignature expiration times should be relatively short. A cosignature expiration time between 5 and 10 minutes is suggested. If the expiration time is too short, transactions may fail because the expiration time elapses before a transaction has been confirmed. However, if the expiration time is too long a leak of the co-signature could be executed well into the future. ```js Cosignature( uint8 v, bytes32 r, bytes32 s, uint256 expiration, address taker ) ``` # Cancelling Orders ## Cancelling A Single Order 1. Display maker's orders (listings and/or offers). 2. Maker selects an order to cancel. 3. If standard order, maker confirms transaction for `revokeSingleNonce` (when order type is fill or kill) or `revokeOrderDigest` (when order type is partially fillable). Revoking a single nonce or order digest requires calldata to be encoded with PaymentProcessorEncoder. 4. If co-signed order flag order as cancelled in DB and do not allow co-signing service to co-sign the order if requested. Order should be hidden from future listing/offer queries. ## Cancelling All Outstanding Standard Orders 1. If the maker chooses to cancel ALL outstanding standard listings and offers they have made, the maker simply needs to confirm a `revokeMasterNonce` transaction. This will cancel/invalidate all prior orders that were signed using their previous master nonce. `revokeMasterNonce` has no calldata requirements and may be called directly by a maker without using PaymentProcessorEncoder. # Cancelling All Outstanding Co-Signed Orders 1. The marketplace should flag all outstanding orders for the user as cancelled in their database. The co-signing service should not return co-signatures for any of the cancelled orders, and the orders should be hidden from all future queries. # Cancelling a Single Permit Order 1. Display maker's permit orders (listings and/or offers). 2. Maker selects an order to cancel. 3. Maker confirms transaction for `invalidateUnorderedNonce` (when the order type is fill or kill) or `closePermittedOrder` (when the order type is partially fillable). ***Note: These operations are executed on Permit-C, not Payment Processor, as that is where permit nonces and permitted partially fillable orders are tracked.*** 4. If co-signed order flag order as cancelled in DB and do not allow co-signing service to co-sign the order if requested. Order should be hidden from future listing/offer queries. # Cancelling ALL Outstanding Permit Orders 1. If the maker chooses to cancel ALL outstanding permit orders, the maker simply needs to confirm a `lockdown` transaction on the permit processor. This will cancel/invalidate all permits, including permits unrelated to Payment Processor. # Cosigner Security Cosignatures on orders allow an order book to offer users the ability to gaslessly cancel their listings and offers. To guard against bad actors order books ***MUST*** keep the private keys of the cosigning accounts secure and ***SHOULD*** use short expirations on cosignatures so that signed but unexecuted cosignatures are unlikely to be used after the maker has cancelled their order off-chain. If the private key to a cosigner account has been exposed the order book ***MUST*** destroy the cosigner through Payment Processor on all chains that it was used as an order cosigner so that it may not be used to fill off-chain cancelled orders. Cosigners cannot be recovered after they have been destroyed on Payment Processor. ## Destroy a Cosigner 1. Use the cosigner private key to sign the message `COSIGNER_SELF_DESTRUCT`. 2. Call `destroyCosigner` on Payment Processor with the cosigner address and message signature. See [encodeDestroyCosigner](./calldata-encoding#encodedestroycosignercalldata). # Encoding Function Call Parameters In order to not overflow smart contract bytecode size limits, Payment Processor is split into smaller library modules. When making calls to Payment Processor, these calls are forwarded to the corresponding module and implementation function using `DELEGATECALL`. For gas efficiency purposes, the function calldata must be passed in as a raw calldata byte array. Payment Processor is deployed with a helper contract called the PaymentProcessorEncoder. It accepts the expected function implementation parameters and encodes them as a single calldata byte array that should be passed into Payment Processor when making calls to fill orders. The subsections below detail each PaymentProcessorEncoder function. ### encodeBuyListingCalldata Used to encode calldata for Payment Processor `buyListing` function. ```solidity function encodeBuyListingCalldata( address paymentProcessorAddress, Order memory saleDetails, SignatureECDSA memory signature, Cosignature memory cosignature, FeeOnTop memory feeOnTop ) external view returns (bytes memory); ``` ### encodeBuyListingAdvancedCalldata Used to encode calldata for PaymentProcessor `buyListingAdvanced` function. ```solidity function encodeBuyListingAdvancedCalldata( address paymentProcessorAddress, AdvancedOrder memory advancedListing, BulkOrderProof memory bulkOrderProof, FeeOnTop memory feeOnTop ) external view returns (bytes memory); ``` ### encodeAcceptOfferCalldata Used to encode calldata for Payment Processor `acceptOffer` function. ```solidity function encodeAcceptOfferCalldata( address paymentProcessorAddress, uint256 offerType, Order memory saleDetails, SignatureECDSA memory signature, TokenSetProof memory tokenSetProof, Cosignature memory cosignature, FeeOnTop memory feeOnTop ) external view returns (bytes memory); ``` *Note: When filling an item offer, `offerType` MUST be set to `1`, and `tokenSetProof` MUST be set to the empty `TokenSetProof({proof: bytes32[](0)})`. *Note: When filling a collection offer, `offerType` MUST be set to `0`, and `tokenSetProof` MUST be set to the empty `TokenSetProof({proof: bytes32[](0)})`. *Note: When filling a token set offer, `offerType` MUST be set to `2`, and `tokenSetProof` MUST contain the valid merkle proof for the collection and token id being filled. ### encodeAcceptOfferAdvancedCalldata Used to encode calldata for the PaymentProcessor `acceptOfferAdvanced` function. ```solidity function encodeAcceptOfferAdvancedCalldata( address paymentProcessorAddress, AdvancedBidOrder memory advancedBid, BulkOrderProof memory bulkOrderProof, FeeOnTop memory feeOnTop, TokenSetProof memory tokenSetProof) external view returns (bytes memory) ``` *Note: If the maker did not use a permit processor, `advancedBid.permitContext` MUST be set to the empty `PermitContext({permitProcessor: address(0), permitNonce: 0})`.* *Note: If the order being filled is not associated with a merkle root, `bulkOrderProof` MUST be set to the empty `BulkOrderProof({orderIndex: 0, proof: []})`.* *Note: If the order being filled does not have fees on top, `feeOnTop` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})`.* ### encodeBulkBuyListingsCalldata Used to encode calldata for Payment Processor `bulkBuyListings` function. ```solidity function encodeBulkBuyListingsCalldata( address paymentProcessorAddress, Order[] calldata saleDetailsArray, SignatureECDSA[] calldata signatures, Cosignature[] calldata cosignatures, FeeOnTop[] calldata feesOnTop ) external view returns (bytes memory); ``` *Note: The length of the `saleDetailsArray`, `signatures`, `cosignatures`, and `feesOnTop` MUST match and MUST be non-zero. `saleDetailsArray[index]` MUST correspond to `signatures[index]`, `cosignatures[index]`, and `feesOnTop[index]`.* *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: As fees on top may not apply to all orders being filled, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})` for any order that is not subject to a fee on top.* ### encodeBulkBuyListingsAdvancedCalldata Used to encode calldata for PaymentProcessor `bulkBuyListingsAdvanced` function. ```solidity function encodeBulkBuyListingsAdvancedCalldata( address paymentProcessorAddress, AdvancedOrder[] memory advancedListingsArray, BulkOrderProof[] memory bulkOrderProofs, FeeOnTop[] memory feesOnTop) external view returns (bytes memory) ``` *Note: The length of the `advancedListingsArray`, `bulkOrderProofs` and `feesOnTop` MUST match and MUST be non-zero. `advancedListingsArray[index]` MUST correspond to `bulkOrderProofs[index]` and `feesOnTop[index]`.* *Note: If the maker did not use a permit processor, `advancedListingsArray[index].permitContext` MUST be set to the empty `PermitContext({permitProcessor: address(0), permitNonce: 0})`.* *Note: If the order being filled is not be associated with a merkle root, `bulkOrderProofs[index]` MUST be set to `BulkOrderProof({orderIndex: 0, proof: []})`.* *Note: If the order being filled does not have fees on top, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})`.* ### encodeBulkAcceptOffersCalldata Used to encode calldata for Payment Processor `bulkAcceptOffers` function. ```solidity function encodeBulkAcceptOffersCalldata( address paymentProcessorAddress, uint256[] memory offerTypeArray, Order[] memory saleDetailsArray, SignatureECDSA[] memory signatures, TokenSetProof[] memory tokenSetProofsArray, Cosignature[] memory cosignaturesArray, FeeOnTop[] memory feesOnTopArray ) external view returns (bytes memory); ``` *Note: The length of the `saleDetailsArray`, `signatures`, `tokenSetProofsArray`, `offerTypeArray`, `cosignatures`, and `feesOnTopArray` MUST match and MUST be non-zero. `saleDetailsArray[index]` MUST correspond to `signatures[index]`, `tokenSetProofsArray[index]`, `offerTypeArray[index]`, `cosignatures[index]`, and `feesOnTopArray[index]`.* *Note: When filling an item offer, `offerTypeArray[index]` MUST be set to `1`, and `tokenSetProofsArray[index]` MUST be set to the empty `TokenSetProof({proof: bytes32[](0)})`. *Note: When filling a collection offer, `offerTypeArray[index]` MUST be set to `0`, and `tokenSetProofsArray[index]` MUST be set to the empty `TokenSetProof({proof: bytes32[](0)})`. *Note: When filling a token set offer, `offerTypeArray[index]` MUST be set to `2`, and `tokenSetProofsArray[index]` MUST contain the valid merkle proof for the collection and token id being filled. *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: As fees on top may not apply to all orders being filled, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})` for any order that is not subject to a fee on top.* ### encodeBulkAcceptOffersAdvancedCalldata Used to encode calldata for PaymentProcessor `bulkAcceptOffersAdvanced` function. ```solidity function encodeBulkAcceptOffersAdvancedCalldata( address paymentProcessorAddress, AdvancedBidOrder[] memory advancedBidsArray, BulkOrderProof[] memory bulkOrderProofs, FeeOnTop[] memory feesOnTop, TokenSetProof[] memory tokenSetProofs) external view returns (bytes memory) ``` *Note: The length of the `advancedBidsArray`, `bulkOrderProofs`, `feesOnTop`, and `tokenSetProofs` MUST match and MUST be non-zero. `advancedBidsArray[index]` MUST correspond to `bulkOrderProofs[index]`, `feesOnTop[index]`, and `tokenSetProofs[index]`.* *Note: If the maker did not use a permit processor, `advancedBidsArray[index].permitContext` MUST be set to the empty `PermitContext({permitProcessor: address(0), permitNonce: 0})`.* *Note: If the order being filled is not be associated with a merkle root, `bulkOrderProofs[index]` MUST be set to the empty `BulkOrderProof({orderIndex: 0, proof: []})`.* *Note: As fees on top may not apply to all orders being filled, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})` for any order that is not subject to a fee on top.* *Note: When filling a token set offer, `tokenSetProofs[index]` MUST be set to the empty `TokenSetProof({proof: bytes32[](0)})`.* ### encodeSweepCollectionCalldata Used to encode calldata for Payment Processor `sweepCollection` function. ```solidity function encodeSweepCollectionCalldata( address paymentProcessorAddress, FeeOnTop memory feeOnTop, SweepOrder memory sweepOrder, SweepItem[] calldata items, SignatureECDSA[] calldata signatures, Cosignature[] calldata cosignatures ) external view returns (bytes memory); ``` *Note: The length of the `items`, `signatures`, and `cosignatures` MUST match and MUST be non-zero. `items[index]` MUST correspond to `signatures[index]` and `cosignatures[index]`.* *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: A single fee on top may be applied to the entire sweep order.* ### encodeSweepCollectionAdvancedCalldata Used to encode calldata for PaymentProcessor `sweepCollectionAdvanced` fucntion. ```solidity function encodeSweepCollectionAdvancedCalldata( address paymentProcessorAddress, AdvancedSweep memory advancedSweep) external view returns (bytes memory) ``` *Note: If the maker did not use a permit processor, `advancedSweep.items[index].permitContext` MUST be set to the empty `PermitContext({permitProcessor: address(0), permitNonce: 0})`.* *Note: If the order being filled is not be associated with a merkle root, `advancedSweep.items[index].bulkOrderProof` MUST be set to the empty `BulkOrderProof({orderIndex: 0, proof: []})`.* *Note: If the order being filled does not have fees on top, `advancedSweep.feeOnTop` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})`.* ### encodeRevokeSingleNonceCalldata Used to encode calldata for Payment Processor `revokeSingleNonce` function (on-chain order cancellation). ```solidity function encodeRevokeSingleNonceCalldata(address paymentProcessorAddress, uint256 nonce) external view returns (bytes memory); ``` ### encodeRevokeOrderDigestCalldata Used to encode calldata for Payment Processor `revokeOrderDigest` function (on-chain order cancellation). ```solidity function encodeRevokeOrderDigestCalldata(address paymentProcessorAddress, bytes32 digest) external view returns (bytes memory); ``` ### encodeDestroyCosignerCalldata Used to encode calldata for Payment Processor `destroyCosignerCalldata` function. ```solidity function encodeDestroyCosignerCalldata( address paymentProcessorAddress, address cosigner, SignatureECDSA memory signature ) external view returns (bytes memory); ``` # Bulk Orders Bulk Order Proofs allow for users to sign for multiple listings or offers in one signature. To make it more transparent, the marketplace should present the signature digest in a human readable format, allowing for users to review the data they sign before completing. Due to how `EIP-712` struct hashing works, a nested array of `[2] * treeHeight` will produce the same hash as the root hash, provided it contains the same data and is padded with empty structs. Trees of depths 1 (`2^1 = 2`) to 10 (`2^10 = 1024`) are supported on Payment Processor. ### Bulk Order Typehashes The Bulk Order Typehash Lookup tables are generated by concatenating the hashes from each height into one bytes array. This is decoded by taking the bytes32 output of the offset of `height - 1 * 32`. ```js // keccack256("BulkSaleApproval(SaleApproval[2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_1_HASH = 0x8659f55d4c88f84030fe09241169834754ebf2e61099b616e530c6b65712c644; // keccack256("BulkSaleApproval(SaleApproval[2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_2_HASH = 0x5106f4043cda72e04ca2c760016628ea8d5667309323ba590682ea5254b4e82e; // keccack256("BulkSaleApproval(SaleApproval[2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_3_HASH = 0x71267a8f42e7ccde4ac75848a93b741df7a2f7a58e40274055f2fa51dbaa69ce; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_4_HASH = 0x6e90557aed2a349f8b6efb2b652c4025c56cfa82424c5cd0c6801cc234ebf519; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_5_HASH = 0xcaf42fc49ad57705ea6be2ca9e1835c9ccddf7bc6fdea8f851917329b1d53a7f; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_6_HASH = 0x09b32b6b5ffee898d5fe18398652042cc8c46449329c5a1a79e425f4ede9bc2b; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_7_HASH = 0xc2362dea3338b88607906397acfdcc5651f19b40a038ee7c89dc4ab54c736cac; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_8_HASH = 0xf2bbc4f6d7ccfa2b8aab6505f0c8f2868c5a190c6c4cfce8c0de447804904df9; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_9_HASH = 0x81f99e15003039550d3597e34e6426b1e230992a5a3727cb5e230754f18f3566; // keccack256("BulkSaleApproval(SaleApproval[2][2][2][2][2][2][2][2][2][2] tree)SaleApproval(uint8 protocol,address cosigner,address seller,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 maxRoyaltyFeeNumerator,uint256 nonce,uint256 masterNonce,uint256 protocolFeeVersion)") bytes32 constant BULK_SALE_APPROVAL_HEIGHT_10_HASH = 0x012178d0a6b8f8748e48411572bdc391435539737aafc91fcba6f2c24ae156cf; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_1_HASH = 0xfb1dd652f3a5dcde3cb90b0cbb7ff0d33df15a48f216e6ac0e2306ff3538e336; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_2_HASH = 0x32fc12675167bd2e9c23f1a9b90ce9c0a14daa7e8b3fa5a01105804ad64aa49d; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_3_HASH = 0x22beb101a4a232e5b05a7b5f4727888724dd418194d6c5b1f1ba5ead4cbeed42; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_4_HASH = 0x9ba46e6a390d8f8edfb6f752177c215b1cf8b6dcf1f5da395ead6c3e19755698; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_5_HASH = 0xae93fe4a03206f6bb45035fab04a39964c83e7d265e93fc31521c86dd25a3be4; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_6_HASH = 0xee3cb9e25b214af21b0d321776864470916324653397b355b267b2894a009a98; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_7_HASH = 0x516d5d5306f95a17cb6d0b84e71f78919a98ac5e3c723fc4b9b1a720193d590d; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_8_HASH = 0xf5b1dcad1bd5c8161d6b904513bc658d4c9b8c288b2271e0f27d5938654e4186; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_9_HASH = 0x63c21e3ce7e506548c01ef4e6a27a5805f9328e0da83e9a7c765dc28266a5aa1; // keccack256("BulkCollectionOfferApproval(CollectionOfferApproval[2][2][2][2][2][2][2][2][2][2] tree)CollectionOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_COLLECTION_OFFER_APPROVAL_HEIGHT_10_HASH = 0x9802b2e35d72255f2ae69bed829f0104723aa156059e02dfccfe781ffc955356; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_1_HASH = 0x243dc86e63d73b10b2764253a38ab8763ded4ac63a632adef4019e482161964a; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_2_HASH = 0x2ed4880b2bfec74882da06ca9f461158c42fa73fa5974b5abec9fcde7cdd43ee; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_3_HASH = 0x8308607f65c90c023abf48563d2c8bd8c783d1db48ffd02ec324105d9f682f96; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_4_HASH = 0x1f7a30dcec9205ff30a5290b3b756e1fd1a1585539bccbc817ab4e59d69a055e; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_5_HASH = 0xcfa8c0abdff5fb2704d1818f38dc974f5bdde6dfe1a4ddc3e22a1f3e593e98f3; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_6_HASH = 0x24e3124a40922800d1eb1890240fc7d07f257a303bb383e8c0820e9910be1f97; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_7_HASH = 0xc72769ff919066b8d1f586bb8b7e720ad93f735ffa9570f9e9ef0e865d13251d; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_8_HASH = 0x89338818bfd140179260dbdc6d6bf38461d49661c166f2b76d3b56727acab7ad; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_9_HASH = 0xf3e381ed4335a28fa612f4068c58de673cad05c8a7fbfb5c1fd557c2c5a2de9f; // keccack256("BulkItemOfferApproval(ItemOfferApproval[2][2][2][2][2][2][2][2][2][2] tree)ItemOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 tokenId,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce)") bytes32 constant BULK_ITEM_OFFER_APPROVAL_HEIGHT_10_HASH = 0x84c94a0cfd84379f92ed5827593e2218a1dc1b975626bbe5c6948b9d2232f7d7; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_1_HASH = 0x0d0b16c0b281480a518e0ae1543bf94c612edc289e45edc04e8510dac617afa4; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_2_HASH = 0x337b9c051f65323200a3580d4917087d4d82e350fcc68b3397c654c9fc7872c3; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_3_HASH = 0x8f8f306a409d08a43b82756f57beb839c3bf5bdd388ba0850d9d21f5c1d0c4dc; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_4_HASH = 0x9f510b8ccf7bbda5c9bd4333d7dcfc8e280aac9eaa9427dcd2f2ffca1fa7a833; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_5_HASH = 0x8599a8fc89aae892d7419812435866d931c9da91198b2f48d1d5318acb524461; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_6_HASH = 0xa0f24864e615845dfa14bee5d4fa28950fcdcf9d31893c68c26f4cfa13402a97; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_7_HASH = 0x621dd1d4cc7477f0587a16f5cd40ea8750209653e7275ccaea3b68623860181b; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_8_HASH = 0x51e570aee92287102d49a7493d337e8aa332c1bc639273de546b428973379fa9; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_9_HASH = 0xa9415346b8de2006517a8de68b2b4147a1d6fd91ce600f4040941627c1c8c3df; // keccack256("BulkTokenSetOfferApproval(TokenSetOfferApproval[2][2][2][2][2][2][2][2][2][2] tree)TokenSetOfferApproval(uint8 protocol,address cosigner,address buyer,address beneficiary,address marketplace,address fallbackRoyaltyRecipient,address paymentMethod,address tokenAddress,uint256 amount,uint256 itemPrice,uint256 expiration,uint256 marketplaceFeeNumerator,uint256 nonce,uint256 masterNonce,bytes32 tokenSetMerkleRoot)") bytes32 constant BULK_TOKEN_SET_OFFER_APPROVAL_HEIGHT_10_HASH = 0xb75be8e10d720cf92518936f56974bdfa8e4a723902dd12e415ffba6eb6dde12; ``` ### Bulk Approval Signature Creation Creating a Bulk Order Signature requires you to create a merkle tree where the root is the bulk order, and the leaves are the approval types (e.g. `SaleApproval` or `ItemOfferApproval`). The tree must be **exactly** 2^N items, where N is the height of the tree calculated by taking the maximum of `log2(orderCount)`. If the amount of approvals is not exactly equal to the computed amount of required items, you would pad with empty approvals which would be unfillable. An example for an order with 7 items would be: Visual Order ``` log2(7) = 2.8 = 3 |---------------------------------------------------| 1 | [[[sa1,sa2], [sa3,sa4]], [[sa5,sa6], [sa7,pad]]] | |---------------------------------------------------| 2 | [[sa1,sa2], [sa3,sa4]] | [[sa5,sa6], [sa7,pad]] | |---------------------------------------------------| 3 | [sa1, sa2] | [sa3, sa4] | [sa5, sa6] | [sa7, pad] | ``` Example in JavaScript ```javascript // Bulk Order Type export const EIP_712_BULK_ORDER_TYPE = { BulkSaleApproval: [{ name: "tree", type: "SaleApproval[2][2][2]" }], SaleApproval: [ { name: "protocol", type: "uint8" }, { name: "cosigner", type: "address" }, { name: "seller", type: "address" }, { name: "marketplace", type: "address" }, { name: "fallbackRoyaltyRecipient", type: "address" }, { name: "paymentMethod", type: "address" }, { name: "tokenAddress", type: "address" }, { name: "tokenId", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "itemPrice", type: "uint256" }, { name: "expiration", type: "uint256" }, { name: "marketplaceFeeNumerator", type: "uint256" }, { name: "maxRoyaltyFeeNumerator", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "masterNonce", type: "uint256" }, { name: "protocolFeeVersion", type: "uint256" } ] }; const bulkArray: SaleApproval[][][] = []; const exampleSaleApproval = { protocol: 0, cosigner: zeroAddress, seller: '0xabc...', marketplace: '0xbeef...', fallbackRoyaltyRecipient: zeroAddress, paymentMethod: zeroAddress, tokenAddress: '0x123...', tokenId: 1, amount: 1, itemPrice: 1e18, expiration: 9999999999999, marketplaceFeeNumerator: 0, maxRoyaltyFeeNumerator: 0, nonce: 0, masterNonce: 0, protocolFeeVersion: 0 }; const blankApproval = { protocol: 0, cosigner: zeroAddress, seller: zeroAddress, marketplace: zeroAddress, fallbackRoyaltyRecipient: zeroAddress, paymentMethod: zeroAddress, tokenAddress: zeroAddress, tokenId: 0, amount: 0, itemPrice: 0, expiration: 0, marketplaceFeeNumerator: 0, maxRoyaltyFeeNumerator: 0, nonce: 0, masterNonce: 0, protocolFeeVersion: 0 } bulkArray[0][0][0] = exampleSaleApproval; bulkArray[0][0][1] = exampleSaleApproval; bulkArray[0][1][0] = exampleSaleApproval; bulkArray[0][1][1] = exampleSaleApproval; bulkArray[1][0][0] = exampleSaleApproval; bulkArray[1][0][1] = exampleSaleApproval; bulkArray[1][1][0] = exampleSaleApproval; bulkArray[1][1][1] = blankApproval; const dataToSign = { BulkSaleApproval: { tree: bulkArray; } } const typedData = JSON.stringify({ primaryType: 'BulkSaleApproval', domain: domain, message: { tree: dataToSign }, types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' } ], ... types, } }); const params = [signerAddress, typedData]; const method = 'eth_signTypedData_v4'; const sig = await window.ethereum.request({ method, params }); // Generate tree const hashedData = dataToSign.flat(Infinity).map((saleApproval) => { const encodedData = encodeAbiParameters( parseAbiParameters( "bytes32 hash, uint8 protocol, address cosigner, address seller, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 maxRoyaltyFeeNumerator, uint256 nonce, uint256 masterNonce, uint256 protocolFeeVersion"), [ SALE_APPROVAL_TYPE_HASH, saleApproval.protocol, saleApproval.cosigner, saleApproval.seller, saleApproval.marketplace, saleApproval.fallbackRoyaltyRecipient, saleApproval.paymentMethod, saleApproval.tokenAddress, saleApproval.tokenId, saleApproval.amount, saleApproval.itemPrice, saleApproval.expiration, saleApproval.marketplaceFeeNumerator, saleApproval.maxRoyaltyFeeNumerator, saleApproval.nonce, saleApproval.masterNonce, saleApproval.protocolFeeVersion ] ); return keccak256(encodedData); }); const tree: MerkleTree = new MerkleTree(hashedData, keccak256, { complete: true, sort: false, hashLeaves: false, fillDefaultHash: keccak256(encodedNullSaleApproval) }); ``` ### Processing Bulk Orders When submitting an order that is part of a bulk approval, the index of the approval must be provided with the proof. This is used to determine the placing of the approval in order to recreate the tree and generate the correct hash. Using the previous example, orders are indexed as below: ``` bulkArray[0][0][0] == 0; bulkArray[0][0][1] == 1; bulkArray[0][1][0] == 2; bulkArray[0][1][1] == 3; bulkArray[1][0][0] == 4; bulkArray[1][0][1] == 5; bulkArray[1][1][0] == 6; bulkArray[1][1][1] == 7; ``` So to submit an order for `bulkArray[0][1][0]` you would submit as below: ```javascript const index = 2; const hexProof = tree.getHexProot(hashedData[index]); const order: AdvancedOrder = { saleDetails: order, signature: sig, cosignature: nullSig, permitContext: { permitProcessor: zeroAddress, permitNonce: BigInt(0), } } const proof: Proof = { proof: hexProof, orderIndex: index }; const feeOnTop: FeeOnTop = { recipient: zeroAddress, amount: BigInt(0), } const calldata = await publicClient.readContract({ address: PAYMENT_PROCESSOR_ENCODER_ADDRESS, abi: paymentProcessorEncoderAbi.abi, functionName: 'encodebuyListingAdvancedCalldata', args: [PAYMENT_PROCESSOR_ADDRESS, order, proof, feeOnTop] }); const { request } = await publicClient.simulateContract({ account: address, address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorAbi.abi, functionName: 'buyListingAdvanced', args: [calldata] }); await walletClient.writeContract(request); ``` # Permits Payment Processor has support to use a permit processor like PermitC to handle approvals in a more advanced and secure way. To handle this, the taker must first give standard approval on their token to the permit processor contract before executing the order. A signature for the permit processor would utilize the `PermitTransferFromWithAdditionalData` on PermitC. ### Features - **Time Bound Approvals:** Approvals via PermitC include an expiration timestamp, allowing for an approval to be only set for a specific period of time. If it’s used past the expiration timestamp, it will be invalid. - **One Click Approval Revoke:** To quickly revoke all approvals, you can call a single function lockdown which will invalidate all outstanding approvals. - **Signature Based Approvals:** Signatures can be provided for operators to increase approval on chain for multi-use approvals, or permit transfer for one off signatures. - **Additional Data Validation:** Signatures can use the permitTransferFromWithAdditionalData calls to append additional validation logic, such as approving only a specific order or set of orders without opening approvals to other functions on the operator contract. - **Unordered Execution:** Nonces retain the same functionality as Permit2, meaning they can be approved and executed in any order and prevent replay. - **Order Based Approvals:** Reusable signatures scoped to a specific order ID to allow for partial trades / fills allowing for gasless outstanding orders to be leveraged. Useful in ERC1155 sales and ERC20 limit orders. ### Frontend Integration Frontends can directly integrate by prompting the maker with a signature that includes the `SaleApproval` or `OfferApproval` as additional data to PermitC. This will be provided to the taker to execute the order, and PermitC will be used to validate the signature and move the tokens. A mock JS integration would look like this: ```javascript const PERMIT_C_TYPE = { PermitTransferFromWithAdditionalData: [ { name: "token", type: "address" }, { name: "id", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "operator", type: "address" }, { name: "expiration", type: "uint256" }, { name: "masterNonce", type: "uint256" }, { name: "approval", type: "SaleApproval" }, ], SaleApproval: [ { name: "protocol", type: "uint8" }, { name: "cosigner", type: "address" }, { name: "seller", type: "address" }, { name: "marketplace", type: "address" }, { name: "fallbackRoyaltyRecipient", type: "address" }, { name: "paymentMethod", type: "address" }, { name: "tokenAddress", type: "address" }, { name: "tokenId", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "itemPrice", type: "uint256" }, { name: "expiration", type: "uint256" }, { name: "marketplaceFeeNumerator", type: "uint256" }, { name: "maxRoyaltyFeeNumerator", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "masterNonce", type: "uint256" }, { name: "protocolFeeVersion", type: "protocolFeeVersion" } ] } const permitNonce = getNextPermitNonce(); const typedData = JSON.stringify({ primaryType: 'PermitTransferFromWithAdditionalData', domain: PERMIT_C_DOMAIN, message: { token: sa1.tokenAddress, id: sa1.tokenId, amount: sa1.amount, nonce: permitNonce, operator: PAYMENT_PROCESSOR_ADDRESS, expiration: sa1.expiration, masterNonce: sa1.masterNonce, approval: sa1 }, types: { EIP712Domain: [ { name: 'name', type: 'string' }, { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' } ], ... PERMIT_C_TYPE, } }); const params = [address, typedData]; const method = 'eth_signTypedData_v4'; const sig = await window.ethereum.request({ method, params }); setSignature(sig); const order: AdvancedOrder = { saleDetails: order1, signature: sig, cosignature: nullSig, permitContext: { permitProcessor: PERMIT_C_ADDRESS, permitNonce: BigInt(permitNonce), } } const nullFeeOnTop: FeeOnTop = { recipient: zeroAddress, amount: BigInt(0), }; const nullBulkOrder: Proof = { orderIndex: BigInt(0), proof: [], }; const calldata = await publicClient?.readContract({ address: PAYMENT_PROCESSOR_ENCODER_ADDRESS, abi: paymentProcessorEncoderAbi.abi, functionName: 'encodeBuyListingAdvancedCalldata', args: [PAYMENT_PROCESSOR_ADDRESS, order, nullBulkOrder, nullFeeOnTop] }); const {request} = await publicClient.simulateContract({ account: address, address: PAYMENT_PROCESSOR_ADDRESS, abi: paymentProcessorAbi.abi, functionName: 'buyListingAdvanced', args: [calldata] }); await walletClient.writeContract(request); ``` # Nonces Payment Processor uses two types of nonces to manage orders - master nonces and order nonces. Every order maker on Payment Processor has their own master nonce and set of order nonces. The master nonce defaults to zero for an order maker and allows the maker to cancel all open orders that were signed using that master nonce by calling revokeMasterNonce on Payment Processor. revokeMasterNonce will increment the maker's master nonce by one and emit a MasterNonceInvalidated event with the order maker's address and the master nonce that was revoked, the maker's new master nonce will be the revoked nonce plus one. Master nonce is part of an order signature and will result in all previously signed orders with that nonce to recover as a different signer and revert. An order nonce is a 32 byte integer that is used to prevent the replay of orders on Payment Processor and allow for orders to be explicitly cancelled prior to fulfillment by the order maker. When a nonce is consumed by fulfillment or cancellation it cannot be used again. ## How are nonces stored? Order nonces are stored in a bitmap data structure for each order maker where each storage slot is a "bucket" of 256 nonces where nonces 0-255 are in the first bucket, 256-511 are in the second bucket, and so forth. Nonces are packed this way for efficient use of storage within the EVM and to reduce gas costs on transaction execution. At a high level - consuming the first nonce in a bucket costs 20,000 gas units, each additional nonce consumed from the same bucket in the same transaction costs 100 gas units (a 99.5% savings over one slot per nonce or using nonces from different buckets), and an additional nonce consumed from a bucket that previously had a nonce consumed but in a different transaction costs 5,000 gas units (75% saving over one slot per nonce or using nonces from different buckets). Payment Processor does not account for the order book/marketplace in nonce validation - order books must generate nonces for users that will not overlap with nonces created by another order book to prevent fulfilled orders on one order book from interfering with open orders on the other. ## What is an ideal nonce? An ideal nonce for a Payment Processor order will include as many zero bytes in the 32 byte nonce as possible to save calldata gas cost (a zero byte in calldata costs 4 gas units while a non-zero byte costs 16 gas units), pack as many fillable order nonces into a 256 nonce "bucket" as possible, and not overlap with nonces used by other order books. To meet these objectives of an ideal nonce we recommend using the upper 4 bytes of the 32 byte nonce value as a marketplace differentiator and incrementing the nonce used for order signing by one for each marketplace as a user signs an order as a simple method for generating an order nonce. ### Example ```js maker = "0x1234...FFFF" orderbookDifferentiator = keccak256("YOUR_ORDER_BOOK") << 224 // 0x3cd2fd820000...0000 orderMakerNextOrderbookNonce = getNextNonce(orderbookDifferentiator, maker) // query database for next nonce, for example assume this value is 123 orderNonce = orderbookDifferentiator | orderMakerNextOrderbookNonce // 0x3cd2fd820000..007B ``` A more advanced solution could track prior nonces used for order signing where another factor on the order, such as expiration time or master nonce revocation, has made a prior order unfillable and re-use those nonces to better pack fulfilled nonces. # Contract Deployments Payment Processor v3.0.0 can be permissionlessly deployed by any wallet to the same addresses on any EVM compatible chain. #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Payment Processor V3.0.0 | `0x9a1D00000000fC540e2000560054812452eB5366` | | Payment Processor V3.0.0 Encoder | `0x9A1D00C3a699f491037745393a0592AC6b62421D` | | Payment Processor V3.0.0 Configuration | `0x9A1D00773287950891B8A48Be6f21e951EFF91b3` | | Payment Processor Module On Chain Cancellation | `0x9A1D005Da1E3daBCE14bc9734DEe692A8978c71C` | | Payment Processor Module Accept Offers | `0x9A1D00E769A108df1cbC3bFfcF867B64ba2E9eFf` | | Payment Processor Module Buy Listings | `0x9A1D00A68523b8268414E4406268c32EC83323A9` | | Payment Processor Module Sweeps | `0x9A1D008994E8f69C66d99B743BDFc6990a7801aB` | | Collection Settings Registry | `0x9A1D001A842c5e6C74b33F2aeEdEc07F0Cb20BC4` | ***Note: If Payment Processor is not yet deployed on your chain, use [the infrastructure deployment tool at developers.apptokens.com/infrastructure.](https://developers.apptokens.com/infrastructure)*** # Payment Processor V2 # Known Limitations Payment Processor was designed to maximize safety and efficency while enforcing creator-defined royalties. As a result, there are certain intentional limitations documented below that creators should be aware of. ## Push Payment Splitters Are Not Supported Push Payment Splitters are smart contracts that receive native token funds, split and redistribute them to one or more addresses within the same transaction they are received. Payment Processor limits the amount of gas that a payment recipient can utilize to 8,000 gas units which is not enough to operate a Push Payment Splitter. **Justification:** 1. ***Security*** - Forwarding of all available gas allows for recipients of transfers to execute additional, potentially malicious, logic. 2. ***Push Payments Are An Anti-pattern*** - The forwarding of native tokens to many destinations in a single transaction has long been considered an anti-pattern. Any one of the recipients could be a re-entrant attacker, or a destination could intentionally revert to grief all the other destinations and DoS a transaction. 3. ***Fairness*** - Without a gas limit, a push payment splitter could be used to do an arbitrary number of splits. We strongly believe it is unfair to the order takers for them to pay an additional ~10K gas units per split destination with an unbounded limit on the number of destinations. If all transaction gas is forwarded with a payment to a push payment splitter with ten recipients the average transaction cost would double. This is not good for traders executing orders and not efficient for the blockchain. **Recommendations for creators:** Use an OpenZeppelin pull payment splitter as your royalty recipient. It is audited and easy to deploy: [OpenZeppelin PaymentSplitter](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.5/contracts/finance/PaymentSplitter.sol) ## Batch Listings And Transfers Are Not Supported Batch listings and transfers have been used to steal millions of dollars worth of NFTs in single transactions from one errant click by a token owner. Payment Processor was intentionally designed to require a signature on each item being listed to provide more secure trading infrastructure. Order takers may still execute orders in bulk. # Payment Settings The simplest way to manage your collection's payment settings is through [developers.freenft.com](https://developers.freenft.com). For creators that would like to build their own integrations for managing collection payment settings - the sections below describe the functions and parameters that Payment Processor uses to apply rules to collection orders. ## Payment Settings Functions ### createPaymentMethodWhitelist Callable by any account to create a new payment method whitelist. The whitelist will be owned by the address that calls the function. **Note:** Due to contract size limitations and for gas efficiency the `createPaymentMethodWhitelist` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodecreatepaymentmethodwhitelistcalldata) for more information. ```solidity function createPaymentMethodWhitelist( string calldata whitelistName ) external returns (uint32 paymentMethodWhitelistId); ``` ### reassignOwnershipOfPaymentMethodWhitelist Callable by the owner of the whitelist to assign ownership of the whitelist to a new account. **Note:** Due to contract size limitations and for gas efficiency the `reassignOwnershipOfPaymentMethodWhitelist` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodereassignownershipofpaymentmethodwhitelistcalldata) for more information. ```solidity function reassignOwnershipOfPaymentMethodWhitelist( uint32 id, address newOwner ) external; ``` ### renounceOwnershipOfPaymentMethodWhitelist Callable by the owner of the whitelist to renounce ownership of the whitelist. Whitelists with the owner renounced can not be modified. **Note:** Due to contract size limitations and for gas efficiency the `renounceOwnershipOfPaymentMethodWhitelist` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encoderenounceownershipofpaymentmethodwhitelistcalldata) for more information. ```solidity function renounceOwnershipOfPaymentMethodWhitelist(uint32 id) external; ``` ### whitelistPaymentMethod Callable by the owner of the whitelist to add a new payment method. **Note:** Due to contract size limitations and for gas efficiency the `whitelistPaymentMethod` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodewhitelistpaymentmethodcalldata) for more information. ```solidity function whitelistPaymentMethod( uint32 paymentMethodWhitelistId, address paymentMethod ) external; ``` ### unwhitelistPaymentMethod Callable by the owner of the whitelist to remove a payment method. **Note:** Due to contract size limitations and for gas efficiency the `unwhitelistPaymentMethod` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodeunwhitelistpaymentmethodcalldata) for more information. ```solidity function unwhitelistPaymentMethod( uint32 paymentMethodWhitelistId, address paymentMethod ) external; ``` ### setCollectionPaymentSettings Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function will configure the payment settings rules to apply to all orders executed on Payment Processor. - `tokenAddress`: Contract address the payment settings are being set for. - `paymentSettings`: Value from the [PaymentSettings](#paymentsettings) enum that defines the type of rules to apply to orders. Creators may pause trading on their collection by setting this value to `Paused`. - `paymentMethodWhitelistId`: Id for a whitelist that was created with [createPaymentMethodWhitelist](#createpaymentmethodwhitelist). This is only used when `paymentSettings` is set to `CustomPaymentMethodWhitelist`. - `constrainedPricingPaymentMethod`: ERC20 token address for the payment method to use when `paymentSettings` is set to `PricingConstraints`. - `royaltyBackfillNumerator`: Royalty percentage to apply to a sale for a collection that does not utilize ERC2981 for on-chain royalties. Denominated in BPS (ex. value of 250 is 2.5% royalty, value of 1,000 is 10% royalty) with a maximum allowed value of 10,000. - `royaltyBackfillReceiver`: Recipient of royalty payments for a collection that does not utilize ERC2981 for on-chain royalties. - `royaltyBountyNumerator`: Percentage of the royalty amount to pay to the marketplace as a reward. Denominated in BPS (ex. value of 2,500 is 25% of the royalty amount, value of 5,000 is 50% of the royalty amount) with a maximum allowed value of 10,000. - `exclusiveBountyReceiver`: Restricts royalty bounty payments to a single marketplace set by this parameter. If exclusiveBountyReceiver is set to `address(0)` any marketplace can receive royalty bounties for the collection. - `blockTradesFromUntrustedChannels`: Boolean flag that when set to true will require order taker transactions to route through a [Trusted Forwarder](../../../trusted-forwarder/overview) that has been approved for the collection using the [addTrustedChannelForCollection](#addtrustedchannelforcollection) function. - `blockBannedAccounts`: Boolean flag that when set to true will block accounts that are banned by the collection owner from being the maker or taker on an order. **Note:** Due to contract size limitations and for gas efficiency the `setCollectionPaymentSettings` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodesetcollectionpaymentsettingscalldata) for more information. ```solidity function setCollectionPaymentSettings( address tokenAddress, PaymentSettings paymentSettings, uint32 paymentMethodWhitelistId, address constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool blockBannedAccounts ) external; ``` ### setCollectionPricingBounds Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function sets the floor and ceiling price for a collection that is applied when the collection's PaymentSettings is set to `PricingConstraints`. Token pricing bounds set in [setTokenPricingBounds](#settokenpricingbounds) take precedence over collection bounds. See [PricingBounds](#pricingbounds) for struct details. **Note:** Due to contract size limitations and for gas efficiency the `setCollectionPricingBounds` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodesetcollectionpricingboundscalldata) for more information. ```solidity function setCollectionPricingBounds( address tokenAddress, PricingBounds calldata pricingBounds ) external; ``` ### setTokenPricingBounds Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function sets the floor and ceiling price for specific tokens in the collection that are applied when the collection's PaymentSettings is set to `PricingConstraints`. Token pricing bounds take precedence over collection bounds that are set in [setCollectionPricingBounds](#setcollectionpricingbounds). See [PricingBounds](#pricingbounds) for struct details. **Note:** Due to contract size limitations and for gas efficiency the `setTokenPricingBounds` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodesettokenpricingboundscalldata) for more information. ```solidity function setTokenPricingBounds( address tokenAddress, uint256[] calldata tokenIds, PricingBounds[] calldata pricingBounds ) external; ``` ### addTrustedChannelForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function approves a channel to be allowed to execute orders for the collection when `blockTradesFromUntrustedChannels` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). See [Trusted Forwarder](../../../trusted-forwarder/overview) for more information on channels. **Note:** Due to contract size limitations and for gas efficiency the `addTrustedChannelForCollection` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodeaddtrustedchannelforcollectioncalldata) for more information. ```solidity function addTrustedChannelForCollection( address tokenAddress, address channel ) external; ``` ### removeTrustedChannelForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function removes a channel's approval to execute orders for the collection when `blockTradesFromUntrustedChannels` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). See [Trusted Forwarder](../../../trusted-forwarder/overview) for more information on channels. **Note:** Due to contract size limitations and for gas efficiency the `removeTrustedChannelForCollection` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encoderemovetrustedchannelforcollectioncalldata) for more information. ```solidity function removeTrustedChannelForCollection( address tokenAddress, address channel ) external; ``` ### addBannedAccountForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function bans an account from being the maker or taker for an order for the collection when `blockBannedAccounts` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). **Note:** Due to contract size limitations and for gas efficiency the `addBannedAccountForCollection` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encodeaddbannedaccountforcollectioncalldata) for more information. ```solidity function addBannedAccountForCollection( address tokenAddress, address account ) external; ``` ### removeBannedAccountForCollection Callable by the token contract, the owner of the token contract or any account assigned the default admin role for the token contract. This function lifts a ban for an account allowing the account to be the maker or taker for an order for the collection when `blockBannedAccounts` is set to true in [setCollectionPaymentSettings](#setcollectionpaymentsettings). **Note:** Due to contract size limitations and for gas efficiency the `removeBannedAccountForCollection` function on Payment Processor must be called with a single calldata byte array that contains what would be the `msg.data` for the below function. See [Encoding Details](./calldata-encoding#encoderemovebannedaccountforcollectioncalldata) for more information. ```solidity function removeBannedAccountForCollection( address tokenAddress, address account ) external; ``` ## Data Structures ### PricingBounds | isSet | floorPrice | ceilingPrice | |-------|------------|--------------| | bool | uint120 | uint120 | - isSet: Boolean true/false flag to indicate the pricing bounds have been set. Distinguishes set zero-value bounds from unset bounds that have default values of zero in the storage slot. - floorPrice: Minimum value to accept for a sales price without rejecting the sale. - ceilingPrice: Maximum value to accept for a sales price without rejecting the sale. ```solidity struct PricingBounds { bool isSet; uint120 floorPrice; uint120 ceilingPrice; } ``` ## Enums ### PaymentSettings - `DefaultPaymentMethodWhitelist` utilizes Payment Processor's default list which includes native token (ex ETH on Ethereum, MATIC on Polygon), wrapped native token, wrapped ETH (if native token is not ETH), and USDC. - `AllowAnyPaymentMethod` allows any payment method (native token or ERC20) for orders. - `CustomPaymentMethodWhitelist` requires the payment method to be one on the whitelist specified in the `paymentMethodWhitelistId` parameter - `PricingConstraints` restricts payments to a single ERC20 specified in the `constrainedPricingPaymentMethod` parameter and enforces sales price is within the pricing bounds specified for a token or the collection (if bounds are not defined for the specific token). Bounds are set with the [setCollectionPricingBounds](#setcollectionpricingbounds) and [setTokenPricingBounds](#settokenpricingbounds) functions. - `Paused` disables all trading on the collection. f ```solidity enum PaymentSettings { DefaultPaymentMethodWhitelist, AllowAnyPaymentMethod, CustomPaymentMethodWhitelist, PricingConstraints, Paused } ``` # Encoding Function Call Parameters In order to not overflow smart contract bytecode size limits, Payment Processor is split into smaller library modules. When making calls to Payment Processor, these calls are forwarded to the corresponding module and implementation function using `DELEGATECALL`. For gas efficiency purposes, the function calldata must be passed in as a raw calldata byte array. Payment Processor is deployed with a helper contract called the PaymentProcessorEncoder. It accepts the expected function implementation parameters and encodes them as a single calldata byte array that should be passed into Payment Processor when making calls to fill orders. The subsections below detail each PaymentProcessorEncoder function. ### encodeCreatePaymentMethodWhitelistCalldata Used to encode calldata for Payment Processor `createPaymentMethodWhitelist` function. ```solidity function encodeCreatePaymentMethodWhitelistCalldata( address paymentProcessorAddress, string calldata whitelistName ) external view returns (bytes memory); ``` ### encodeReassignOwnershipOfPaymentMethodWhitelistCalldata Used to encode calldata for Payment Processor `reassignOwnershipOfPaymentMethodWhitelist` function. ```solidity function encodeReassignOwnershipOfPaymentMethodWhitelistCalldata( address paymentProcessorAddress, uint32 id, address newOwner ) external view returns (bytes memory) ``` ### encodeRenounceOwnershipOfPaymentMethodWhitelistCalldata Used to encode calldata for Payment Processor `renounceOwnershipOfPaymentMethodWhitelist` function. ```solidity function encodeRenounceOwnershipOfPaymentMethodWhitelistCalldata( address paymentProcessorAddress, uint32 id ) external view returns (bytes memory) ``` ### encodeWhitelistPaymentMethodCalldata Used to encode calldata for Payment Processor `whitelistPaymentMethod` function. ```solidity function encodeWhitelistPaymentMethodCalldata( address paymentProcessorAddress, uint32 paymentMethodWhitelistId, address paymentMethod ) external view returns (bytes memory); ``` ### encodeUnwhitelistPaymentMethodCalldata Used to encode calldata for Payment Processor `unwhitelistPaymentMethod` function. ```solidity function encodeUnwhitelistPaymentMethodCalldata( address paymentProcessorAddress, uint32 paymentMethodWhitelistId, address paymentMethod ) external view returns (bytes memory); ``` ### encodeSetCollectionPaymentSettingsCalldata Used to encode calldata for Payment Processor `setCollectionPaymentSettings` function. ```solidity function encodeSetCollectionPaymentSettingsCalldata( address paymentProcessorAddress, address tokenAddress, PaymentSettings paymentSettings, uint32 paymentMethodWhitelistId, address constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool blockBannedAccounts ); ``` ### encodeSetCollectionPricingBoundsCalldata Used to encode calldata for Payment Processor `setCollectionPricingBounds` function. ```solidity function encodeSetCollectionPricingBoundsCalldata( address paymentProcessorAddress, address tokenAddress, PricingBounds calldata pricingBounds ); ``` ### encodeSetTokenPricingBoundsCalldata Used to encode calldata for Payment Processor `setTokenPricingBounds` function. ```solidity function encodeSetTokenPricingBoundsCalldata( address paymentProcessorAddress, address tokenAddress, uint256[] calldata tokenIds, PricingBounds[] calldata pricingBounds ); ``` ### encodeAddTrustedChannelForCollectionCalldata Used to encode calldata for Payment Processor `addTrustedChannelForCollection` function. ```solidity function encodeAddTrustedChannelForCollectionCalldata( address paymentProcessorAddress, address tokenAddress, address channel ) external view returns (bytes memory); ``` ### encodeRemoveTrustedChannelForCollectionCalldata Used to encode calldata for Payment Processor `removeTrustedChannelForCollection` function. ```solidity function encodeRemoveTrustedChannelForCollectionCalldata( address paymentProcessorAddress, address tokenAddress, address channel ) external view returns (bytes memory); ``` ### encodeAddBannedAccountForCollectionCalldata Used to encode calldata for Payment Processor `addBannedAccountForCollection` function. ```solidity function encodeAddBannedAccountForCollectionCalldata( address paymentProcessorAddress, address tokenAddress, address account ) external view returns (bytes memory); ``` ### encodeRemoveBannedAccountForCollectionCalldata Used to encode calldata for Payment Processor `removeBannedAccountForCollection` function. ```solidity function encodeRemoveBannedAccountForCollectionCalldata( address paymentProcessorAddress, address tokenAddress, address account ) external view returns (bytes memory); ``` # Glossary Here are some helpful terms to keep in mind. - **Payment Processor**: An NFT exchange protocol built by creators, for creators. Built for trading of ERC721-C and ERC1155-C tokens, but backwards compatible to support trading of any ERC721 or ERC1155 token as well. Analagous to Blur Marketplace and Seaport exchange protocols, but built entirely around honoring fully on-chain programmable royalties. Also known as `PaymentProcessor`. - **PaymentProcessor**: Shorthand for `Payment Processor`. - **Payment Processor Encoder**: A helper contract deployed alongside Payment Processor that integrated marketplaces use to format Payment Processor function calldata. - **Maker(s)**: Create buying or selling orders that are not carried out immediately. For example, "sell NFT `A` at a price of $100" or "buy NFT `B` for $100". This creates liquidity, meaning that it is easier for others to instantly buy or sell NFTs when the conditions are met. When Bob lists an NFT he owns for sale, Bob is considered maker of the order. Similarly, when Bob offers to buy an NFT, Bob is considered the maker of the order. - **Taker(s)**: The entities that buy or sell instantly are called takers. In other words, the takers fill the orders created by the makers. When Alice executes and order to buy Bob's listing, Alice is considered the taker of the order. Similarly, when Alice accepts an offer from Bob to buy an NFT she owns, Alice is considered the taker of the order. - **Order(s)**: Payment Processor supports two broad categories of orders and four types: - **Standard Order**: Any listing or offer that may be filled directly by a taker. Standard orders, when cancelled, must be cancelled on-chain by the order maker at their own gas expense. Standard orders are not susceptible to censorship provided they are accessible through an orderbook API. - **Cosigned Order**: Any listing or offer that must be cosigned by another party in order to be filled by a taker. Cosigned orders, when cancelled, can be gaslessly cancelled off-chain. Order-book providers that support cosigned orders must properly secure and automate the co-signing process. The order-book provider must guarantee that no cosignature can be generated for a previously cancelled order. Similarly, the order-book provider must guarantee availability of cosignatures to takers such that orders can always be filled uninterrupted. The use of cosigned orders SHOULD be at the order maker's discretion. Pros include cheaper, faster cancellation/replacement of orders without incurring transaction gas costs. Cons include censorship/denial of service risks. - **Listing**: An order (standard or cosigned) to sell an NFT once filled by a taker. Listings are gaslessly signed off-chain by the owner of the NFT. - **Item Offer**: An order (standard or cosigned) to buy a single, specific NFT once filled by a taker. Item offers apply to exactly one specific token id in a collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing the NFT. - **Collection Offer**: An order (standard or cosigned) to buy any NFT belonging to a specific collection once filled by a taker. Collection offers apply to any token id for a specific collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing an NFT. - **Token Set Offer**: An order (standard or cosigned) to buy a single NFT from a subset of token ids in a specific collection once filled by a taker. Token set offers apply to a subset of token ids for a specific collection. Offers are gaslessly signed off-chain by one or more parties interested in purchasing an NFT. - **Beneficiary**: The address of the account that receives the NFT/item when an order is filled. The buyer and beneficiary can be, but don't have to be the same account. Beneficiaries are acknowledged and signed in offer orders when an offer is made. Alternately, the beneficiary is designated and signed in the calldata of the buy transaction when the taker buys a listing. - **Marketplace Fee**: A fee paid to the primary marketplace where an order was made. The marketplace fee is acknowledged in signed orders. - **Fee On Top**: A `fee on top` is typically reserved for a marketplace or specialized wallet that finds orders filled by the taker. This taker marketplace fee is an optional fee paid by the taker in excess of the items' prices in one or more orders. - **Royalty Bounty**: A percentage, optionally paid out of a creator's royalty on each trade, to the marketplace where the NFT order was made. Royalty bounties are optional, and creators can configure a percentage from 0-100% to share with marketplaces that provide liquidity. Royalty bounties are a powerful tool to align incentives between creators and marketplaces. # Events Payment Processor emits several indexible events that should be indexed by off-chain order books and analytics platforms. ## Order Fill Events Off-chain order books and analytics platforms can track the following events for trading volume and metrics, as well as trades by user. ```solidity event BuyListingERC721( address indexed buyer, address indexed seller, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 salePrice); event BuyListingERC1155( address indexed buyer, address indexed seller, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 amount, uint256 salePrice); event AcceptOfferERC721( address indexed seller, address indexed buyer, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 salePrice); event AcceptOfferERC1155( address indexed seller, address indexed buyer, address indexed tokenAddress, address beneficiary, address paymentCoin, uint256 tokenId, uint256 amount, uint256 salePrice); ``` ## Order Cancellation Events Off-chain order books SHOULD index the following events to track what orders have been filled or cancelled and which nonces are un-available. ```solidity event MasterNonceInvalidated( uint256 indexed nonce, address indexed account); event NonceInvalidated( uint256 indexed nonce, address indexed account, bool wasCancellation); event OrderDigestInvalidated( bytes32 indexed orderDigest, address indexed account, bool wasCancellation); ``` ## Cosigner Events Off-chain order books SHOULD index the following event to track when a cosigner has been destroyed and filter any open orders that require a cosignature from the cosigner. ```solidity event DestroyedCosigner(address indexed cosigner); ``` ## Collection Payment Settings Events Off-chain order books SHOULD index collection payment settings events to track allowed payment methods, backfilled royalties, and royalty bounties. ```solidity event UpdatedCollectionPaymentSettings( address indexed tokenAddress, PaymentSettings paymentSettings, uint32 indexed paymentMethodWhitelistId, address indexed constrainedPricingPaymentMethod, uint16 royaltyBackfillNumerator, address royaltyBackfillReceiver, uint16 royaltyBountyNumerator, address exclusiveBountyReceiver, bool blockTradesFromUntrustedChannels, bool blockBannedAccounts); ``` ## Custom Payment Method Whitelist Events Off-chain order books SHOULD index payment method whitelists by id. By doing so, when a collection applies a custom payment method whitelist id, exchanges can be informed about which payment methods are allowed for a give collection. ```solidity event CreatedPaymentMethodWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed whitelistOwner, string whitelistName); event PaymentMethodAddedToWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); event PaymentMethodRemovedFromWhitelist( uint32 indexed paymentMethodWhitelistId, address indexed paymentMethod); event ReassignedPaymentMethodWhitelistOwnership( uint32 indexed id, address indexed newOwner); ``` ## Bounded Pricing Events Off-chain order books SHOULD index pricing bounds events to track min and max allowable sale prices for collections that implement pricing constraints. ```solidity event UpdatedCollectionLevelPricingBoundaries( address indexed tokenAddress, uint256 floorPrice, uint256 ceilingPrice); event UpdatedTokenLevelPricingBoundaries( address indexed tokenAddress, uint256 indexed tokenId, uint256 floorPrice, uint256 ceilingPrice); ``` ## Trusted Channel Events Off-chain order books SHOULD index trusted channel events to be used in conjunction with the [`Collection Payment Settings Events`](#collection-payment-settings-events) to determine the allowed channels for a collection if `blockTradesFromUntrustedChannels` is set to `true`. ```solidity event TrustedChannelAddedForCollection( address indexed tokenAddress, address indexed channel); event TrustedChannelRemovedForCollection( address indexed tokenAddress, address indexed channel); ``` ## Banned Account Events Off-chain order books SHOULD index banned account events to be used in conjunction with the [`Collection Payment Settings Events`](#collection-payment-settings-events) to determine if a maker or taker is allowed to execute an order when `blockBannedAccounts` is set to `true`. ```solidity event BannedAccountAddedForCollection( address indexed tokenAddress, address indexed account); event BannedAccountRemovedForCollection( address indexed tokenAddress, address indexed account); ``` # Data Structures The following subsections detail the data structures needed for the creation of and filling of orders in Payment Processor. Note that [maker signature formats vary by order-type](#maker-signature-formats). ### SignatureECDSA | V | R | S | |-------|---------|---------| | uint8 | bytes32 | bytes32 | - V: The `v` component of an ECDSA signature. - R: The `r` component of an ECDSA signature. - S: The `s` component of an ECDSA signature. *[Note: For a detailed explanation of ECDSA signatures read this article](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7).* ### Cosignature | Signer | Taker | Expiration | V | R | S | |---------|---------|------------|-------|---------|---------| | address | address | uint256 | uint8 | bytes32 | bytes32 | - Signer: The co-signer designated and acknowledged in the order maker's signature approving the order. MUST be an EOA, or `address(0)` when the co-signature is empty. It is expected that marketplaces implementing co-signing MUST expose an API enabling the taker to retrieve a co-signature approving the fill of co-signed orders. - Taker: The address of the account filling the co-signed order. May be an EOA or Smart Contract account, or `address(0)` when the co-signature is empty. A marketplace's co-signing API service MUST acknowledge the taker address in the co-signature. - Expiration: The unix timestamp (in seconds) when the co-signature expires, or `0` when the co-signature is empty. A marketplace's co-signing API should use the current unix timestamp and add a validity time that is acknowledged in the co-signature. If the validity time is too short, there might not be sufficient time for the fill transaction to be confirmed on-chain, possibly resulting in failed fill transactions. However, the validity time should also not be too long, as a taker could sit on a fill transaction for quite a while. It is left up to the marketplace to determine an appropriate validity time. - V: The `v` component of the co-signer's signature. - R: The `r` component of the co-signer's signature. - S: The `s` component of the co-signer's signature. *Note: The co-signature is generated when the co-signer creates an ECDSA signature of the [Cosignature](#cosignature-format) typed data message.* ### FeeOnTop A `fee on top` is typically reserved for a marketplace or specialized wallet that found the order filled by the taker. This taker marketplace fee is an optional fee paid by the taker in excess of the items' prices in one or more orders. When the maker and taker marketplace is the same, it is strongly encouraged not to apply this fee, as the fee can already be assessed in the maker fee. Note that the `fee on top` is paid in the same currency as the order's payment method. | Recipient | Amount | |-----------|---------| | address | uint256 | - Recipient: The address to which the fee is paid, or `address(0)` when the fee is empty. - Amount: The absolute amount of the fee, in wei, or `0` when the fee is empty. Note that the fee amount is not permitted to exceed the item sale price of any given order. ### TokenSetProof Token set proofs are required to fill token set offers on a collection. A token set offer is a merkle tree where the leaf data is the keccak256 hash of the collection address and token id. ``` leafHash = keccak256(abi.encode(collectionAddress, tokenId)); ``` | Root Hash | Proof | |-----------|-----------| | bytes32 | bytes32[] | - Root Hash: The root hash of the merkle tree containing 2 or more collection/token id leaf nodes, or `bytes32(0)` when the order being filled is not a token set offer. - Proof: The merkle proof for the specific collection/token id leaf node being filled, , or `bytes32[](0)` when the order being filled is not a token set offer. ### Order This data structure is used to fill all single and bulk orders that are not sweep orders. | Protocol | Maker | Beneficiary | Marketplace | Fallback Royalty Recipient | Payment Method | Token Address | Token Id | Amount | Item Price | Nonce | Expiration | Marketplace Fee Numerator | Max Royalty Fee Numerator | Requested Fill Amount | Minimum Fill Amount | |----------|---------|-------------|-------------|----------------------------|----------------|---------------|----------|---------|------------|---------|------------|---------------------------|---------------------------|-----------------------|---------------------| |uint8 | address | address | address | address | address | address | uint256 | uint248 | uint256 | uint256 | uint256 | uint256 | uint256 | uint248 | uint248 | - Protocol: `0` for `ERC721_FILL_OR_KILL`, `1` for `ERC1155_FILL_OR_KILL`, or `2` for `ERC1155_FILL_PARTIAL` collections. `FILL_PARTIAL` means an order is partially fillable across multiple trades, while `FILL_OR_KILL` means the entire amount specified in the order must be filled in a single transaction. - Maker: The address of the account the created the order. May be an EOA or Smart Contract account. When the order was a listing, the maker is the seller of the item. When the order was an offer, the maker is the buyer of the item. - Beneficiary: The address of the account that receives the item when the order is filled. When the order was a listing, the taker (`msg.sender`) can either buy with themselves as the beneficiary, or another account. When the order was an offer, the maker would be the buyer, and the beneficiary address can either be the maker's account or another account. For example, the buyer could be a user's hot wallet, and the beneficiary could be the user's cold storage wallet. - Marketplace: The address to which the primary (maker) marketplace fee should be paid, or `address(0)` if the maker marketplace charges no platform fees. Note that for collections that offer non-exclusive royalty bounties, the maker marketplace also receives a royalty bounty paid out of creator royalties. For collections that offer exclusive royalty bounties, the maker marketplace receives a royalty bounty only if it matches the exclusive royalty bounty recipient designated by the collection creator. - Fallback Royalty Recipient: Used to allow marketplaces to support royalties for collections that do not implement ERC-2981, do not have a contract owner and do not have a user with default admin role. Fallback royalties are calculated using the `Max Royalty Fee Numerator` for the order. - Payment Method: The address of the ERC-20 coin used to fill the trade, or `address(0)` for the native currency of the chain the trade executed on. For example, `address(0)` denotes ETH on ETH Mainnet, and Matic on Polygon Mainnet. - Token Address: The address of the collection. - Token Id: The id of the token (if collection is ERC721), or the token type id of the token (if collection is ERC1155). - Amount: The number of tokens. MUST be `1` if collection is ERC721, and MUST be greater than or equal to `1` if collection is ERC1155. - Item Price: The price of the token(s) in the order. - Nonce: A unique identifier to the order that prevents replay attacks. Nonce applies only to standard signed orders and may not be re-used. For co-signed order, nonce should be set to `0`. Note: It is easier to generate a random nonce than attempt to keep track of nonces that have been used. However, be aware that a gas optimization is in place such that filled or cancelled nonces are tracked in a bitmap. Nonces 0-255, 256-511, 512-767, etc are stored in the same slot. Each maker address has its own set of nonces generated for gasless listings such that `nonce 1234` is stored separately for makers `address(5678)` and `address(abcd)` but storage of the same nonce for the same maker is common across all marketplaces. It is possible that marketplaces will have no knowledge of nonces used in outstanding order signatures. Marketplaces should employ a nonce sequencing methodology that reduces the probability of overlapping nonce usage with other marketplaces. Examples of this could be a fixed set of upper bits for all orders placed through that marketplace where nonces `0x12340000...0000` to `0x1234FFFF...FFFF` relate to a specific marketplace or the development of a common nonce issuance service that tracks all nonces used by a maker. - Expiration: The unix timestamp (in seconds) when the maker's order signature expires. A marketplace's order making API should use the current unix timestamp and add a user-defined validity time that is acknowledged in the maker's signature. - Marketplace Fee Numerator: Marketplace fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. - Max Royalty Fee Numerator: Maximum approved royalty fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. When requesting the order signature from the order maker, the marketplace MUST first attempt to read the royalties for the individual token using the EIP-2981 `royaltyInfo` function call on the collection. If `royaltyInfo` raises an exception (likely because it is unimplemented), the marketplace MUST attempt to determine if royalties have been backfilled by calling the `collectionRoyaltyBackfillSettings` function on Payment Processor. If no on-chain royalties are present, this may be set to `0`. - Requested/Minimum Fill Amount: When a fill transaction is submitted, this is the number of items the taker wishes to fill. If insufficient items remain from the partially filled order, the remaining number of items is filled instead, provided the number of remaining items its greater than or equal to the Minimum Fill Amount. ### Sweep Order This data structure is used to fill sweep orders, and represents the shared values that apply to all order in the sweep. | Protocol | Token Address | Payment Method | Beneficiary | |----------| --------------|----------------|-------------| | uint8 | address | address | address | - Protocol: `0` for ERC721 or `1` for ERC1155 collections. - Token Address: The address of the collection. - Payment Method: The address of the ERC-20 coin used to fill the trade, or `address(0)` for the native currency of the chain the trade executed on. For example, `address(0)` denotes ETH on ETH Mainnet, and Matic on Polygon Mainnet. - Beneficiary: The address of the account that receives the item when the order is filled. ### Sweep Item This data structure is used to fill sweep orders, and represents the values that apply to individual orders in the sweep. | Maker | Marketplace | Fallback Royalty Recipient | Token Id | Amount | Item Price | Nonce | Expiration | Marketplace Fee Numerator | Max Royalty Fee Numerator | |---------|-------------|----------------------------|----------|------------|------------|---------|------------|---------------------------|---------------------------| | address | address | address | uint256 | uint248 | uint256 | uint256 | uint256 | uint256 | uint256 | - Maker: The address of the account the created the order. May be an EOA or Smart Contract account. When the order was a listing, the maker is the seller of the item. When the order was an offer, the maker is the buyer of the item. - Marketplace: The address to which the primary (maker) marketplace fee should be paid, or `address(0)` if the maker marketplace charges no platform fees. Note that for collections that offer non-exclusive royalty bounties, the maker marketplace also receives a royalty bounty paid out of creator royalties. For collections that offer exclusive royalty bounties, the maker marketplace receives a royalty bounty only if it matches the exclusive royalty bounty recipient designated by the collection creator. - Fallback Royalty Recipient: Used to allow marketplaces to support royalties for collections that do not implement ERC-2981, do not have a contract owner and do not have a user with default admin role. Fallback royalties are calculated using the `Max Royalty Fee Numerator` for the sweep item. - Token Id: The id of the token (if collection is ERC721), or the token type id of the token (if collection is ERC1155). - Amount: The number of tokens. MUST be `1` if collection is ERC721, and MUST be greater than or equal to `1` if collection is ERC1155. - Item Price: The price of the token(s) in the order. - Nonce: A unique identifier to the order that prevents replay attacks. Nonce applies only to standard signed orders and may not be re-used. For co-signed order, nonce should be set to `0`. Note: It is easier to generate a random nonce than attempt to keep track of nonces that have been used. However, be aware that a gas optimization is in place such that filled or cancelled nonces are tracked in a bitmap. Nonces 0-255, 256-511, 512-767, etc are stored in the same slot. Each maker address has its own set of nonces generated for gasless listings such that `nonce 1234` is stored separately for makers `address(5678)` and `address(abcd)` but storage of the same nonce for the same maker is common across all marketplaces. It is possible that marketplaces will have no knowledge of nonces used in outstanding order signatures. Marketplaces should employ a nonce sequencing methodology that reduces the probability of overlapping nonce usage with other marketplaces. Examples of this could be a fixed set of upper bits for all orders placed through that marketplace where nonces `0x12340000...0000` to `0x1234FFFF...FFFF` relate to a specific marketplace or the development of a common nonce issuance service that tracks all nonces used by a maker. - Expiration: The unix timestamp (in seconds) when the maker's order signature expires. A marketplace's order making API should use the current unix timestamp and add a user-defined validity time that is acknowledged in the maker's signature. - Marketplace Fee Numerator: Marketplace fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. - Max Royalty Fee Numerator: Maximum approved royalty fee percentage in bips. Should be in range 0-10,000, as denominator is 10,000. 0.5% fee numerator is 50, 1% fee numerator is 100, 10% fee numerator is 1,000 and so on. When requesting the order signature from the order maker, the marketplace MUST first attempt to read the royalties for the individual token using the EIP-2981 `royaltyInfo` function call on the collection. If `royaltyInfo` raises an exception (likely because it is unimplemented), the marketplace MUST attempt to determine if royalties have been backfilled by calling the `collectionRoyaltyBackfillSettings` function on Payment Processor. If no on-chain royalties are present, this may be set to `0`. # Listings A listing is an order (standard or cosigned) to sell an NFT once filled by a taker. Listings are gaslessly signed off-chain by the owner/seller of the NFT. ## Notional Exchange Listing Flow 1. Maker browses their items and selects an item to list for sale. 2. Maker inputs: Listing Currency/Payment Method, Price, Validity Time. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage and on-chain royalty fee (display sale price, fees, and net seller proceeds) 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 5. Signature Prompt for [SaleApproval](#sale-approval) signature. 6. Order and signature submitted to order book. ## Sale Approval When listing an individual NFT for sale the seller/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the order is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js SaleApproval( uint8 protocol, address cosigner, address seller, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 maxRoyaltyFeeNumerator, uint256 nonce, uint256 masterNonce ) ``` # Offers Offers are gaslessly signed off-chain by one or more parties interested in purchasing the NFT. Payment Processor support three offer types: - **Item Offer**: An order (standard or cosigned) to buy a single, specific NFT once filled by a taker. Item offers apply to exactly one specific token id in a collection. - **Collection Offer**: An order (standard or cosigned) to buy any NFT belonging to a specific collection once filled by a taker. Collection offers apply to any token id for a specific collection. - **Token Set Offer**: An order (standard or cosigned) to buy a single NFT from a subset of token ids in a specific collection once filled by a taker. Token set offers apply to a subset of token ids for a specific collection. ## Notional Exchange Offer Flow ### Item Offer 1. Maker browses a collection and selects a single item to make an offer on. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds) 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). For standard orders, a nonce is generated. 5. Signature Prompt for either [ItemOfferApproval](#item-offer-approval). 6. Order and signature submitted to order book. ### Collection Offer 1. Maker browses a collection and chooses to make a collection offer for any item in the collection. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds). 4. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 5. Signature Prompt for either [CollectionOfferApproval](#collection-offer-approval). 6. Order and signature submitted to order book. ### Token Set Collection Offer This can take a few forms depending on how the marketplace chooses to implement this. This can take the form of offers on traits, offers on tokens that have not been stolen, etc. 1. Maker browses a collection and chooses some criteria for token ids to make an offer on. The marketplace generates a merkle tree of tokens based on the criteria. 2. Maker inputs: Offer Currency/Payment Method, Price, Validity Time, Beneficiary. 3. Maker prompted to acknowledge/accept maker marketplace fee percentage (display sale price, fees, and net seller proceeds). 4. Maker prompted to acknowledge the set of tokens/merkle root. 5. Maker prompted to decide on standard order (on-chain cancellations) or co-signed order (gasless cancellation). 6. Signature Prompt for either [TokenSetOfferApproval](#token-set-offer-approval). 7. Order and signature submitted to order book. ## Item Offer Approval When making an offer on a specific NFT the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js ItemOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 tokenId, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce ) ``` Typed data hash: `0xce2e9706d63e89ddf7ee16ce0508a1c3c9bd1904c582db2e647e6f4690a0bf6b` ## Collection Offer Approval When making an offer on any NFT in a specific collection the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js CollectionOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce ) ``` Typed data hash: `0x8fe9498e93fe26b30ebf76fac07bd4705201c8609227362697082288e3b4af9c` ## Token Set Offer Approval When making an offer on a subset of NFTs in a specific collection the buyer/maker MUST be prompted by the exchange to sign an EIP-712 sale approval in the following format. If the offer is a standard order `cosigner` MUST be set to `address(0)`, for cosigned orders `cosigner` is set to the address that will cosign the order at time of fulfillment. The exchange MUST submit the signature and order data to an integrated order book via its API(s). ```js TokenSetOfferApproval( uint8 protocol, address cosigner, address buyer, address beneficiary, address marketplace, address fallbackRoyaltyRecipient, address paymentMethod, address tokenAddress, uint256 amount, uint256 itemPrice, uint256 expiration, uint256 marketplaceFeeNumerator, uint256 nonce, uint256 masterNonce, bytes32 tokenSetMerkleRoot ) ``` Typed data hash: `0x244905ade6b0e455d12fb539a4b17d7f675db14797d514168d09814a09c70e70` # Filling Orders ## Filling A Single Listing (Buy Now) 1. Taker browses listings and chooses a listed item. 2. Taker selects "Buy Now". 3. Taker selects beneficiary (whether self or other account). 4. Taker prompted to review the details, including review and acknowledgement of fee on top when applicable. 5. If listing order was co-signed, client requests a co-signature for the listing. 6. Marketplace calls the PaymentProcessorEncoder `encodeBuyListingCalldata` function to generate the calldata to fill the order. [Encoding Details](./calldata-encoding#encodebuylistingcalldata) 7. Wallet pops up a transaction confirmation of the `buyListing` call. [Usage Details](#buy-listing) 8. Taker confirms the transaction through their wallet interface. ## Filling A Batch Of Listings (Shopping Cart) 1. Taker browses listings and chooses several listed items (they may be from one or more collections). 2. Taker selects "Add To Shopping Cart" for each item. 3. Taker reviews cart and "Checks Out". 4. Taker selects beneficiary (whether self or other account). 5. Taker prompted to review the details, including review and acknowledgement of all fees on top when applicable. 6. For any co-signed listing orders, client requests co-signatures. 7. Marketplace calls the PaymentProcessorEncoder `encodeBulkBuyListingsCalldata` function to generate the calldata to bulk fill the orders. [Encoding Details](./calldata-encoding#encodebulkbuylistingscalldata) 8. Wallet pops up a transaction confirmation of the `bulkBuyListings` call. [Usage Details](#bulk-buy-listings) 9. Taker confirms the transaction through their wallet interface. ## Filling A Batch of Listings (Collection Sweep) 1. Taker chooses "Sweep Collection" for the desired collection. 2. Taker specifies desired quantity and maximum price per item. 3. Taker selects beneficiary (whether self or other account). 4. Order book supplies listings that are the best matches. 5. Taker prompted to review the details, including review and acknowledgement of the fee on top when applicable. 6. For any co-signed listing orders, client requests co-signatures. 7. Marketplace calls the PaymentProcessorEncoder `encodeSweepCollectionCalldata` function to generate the calldata to fill the sweep. [Encoding Details](./calldata-encoding#encodesweepcollectioncalldata) 8. Wallet pops up a transaction confirmation of the `sweepCollection` call. [Usage Details](#sweep-collection) 9. Taker confirms the transaction through their wallet interface. ## Filling A Single Offer (Accept Offer) 1. Taker browser offers where the offer criteria matches items they own and chooses one. 2. Taker selects "Accept Offer". 3. Taker prompted to review the details, including review and acknowledgement of the current on-chain royalty fee, maker marketplace fee, and fee on top when applicable. 4. If the offer order was co-signed, client requests a co-signature for the offer. 5. Marketplace calls the PaymentProcessorEncoder `encodeAcceptOfferCalldata` function to generate the calldata to fill the order. [Encoding Details](./calldata-encoding#encodeacceptoffercalldata) 6. Wallet pops up a transaction confirmation of the `acceptOffer` call. [Usage Details](#accept-offer) 7. Taker confirms the transaction through their wallet interface. ## Filling A Batch Of Offers (Offer Cart) 1. Taker browser offers where the offer criteria matches items they own and chooses several. 2. Taker selects "Add To Offer Cart" for each item. 3. Taker reviews cart and "Checks Out". 4. Taker prompted to review the details, including review and acknowledgement of the current on-chain royalty fees for each item, the maker marketplace fees for each item, and the fees on top when applicable. 5. For any co-signed offer orders, client requests co-signatures. 6. Marketplace calls the PaymentProcessorEncoder `encodeBulkAcceptOffersCalldata` function to generate the calldata to bulk fill the orders. [Encoding Details](./calldata-encoding#encodebulkacceptofferscalldata) 7. Wallet pops up a transaction confirmation of the `bulkAcceptOffers` call. [Usage Details](#bulk-accept-offers) 8. Taker confirms the transaction through their wallet interface. ***Note: There are other steps marketplaces may need to implement to prompt users in the workflow. For instance: approving Payment Processor to transfer NFTs and ERC-20 payments, prompting to wrap native currency or swap currencies when needed, or prompting buyers to perform a one-time signature to prove they are EOAs [for select ERC721-C security levels only].*** ## Taker Operations / Functions ### Buy Listing Exchanges should call `buyListing` when a taker wants to purchase a single listing using a UX analogous to "Buy Now". There are four order types that may be filled through the `buyListing` function: **Standard Sell Order *Without* Fee On Top** - To be used when the taker fills a standard sell order and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `cosignature` and `feeOnTop` struct parameters passed to PaymentProcessorEncoder would be filled with zero values. **Standard Sell Order *With* Fee On Top** - To be used when the taker fills a standard sell order via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution the `cosignature` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `feeOnTop` struct parameter would contain the `recipient` and `amount` for the fee being added by the taker marketplace. **Cosigned Sell Order *Without* Fee On Top** - To be used when the taker fills a co-signed sell order and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `feeOnTop` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `cosignature` struct parameter would contain the `signer`, `taker`, `expiration` and cosignature `v`, `r` and `s` values for the cosignature. **Cosigned Sell Order *With* Fee On Top** - To be used when the taker fills a co-signed sell order via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution both the `feeOnTop` and `cosignature` struct parameters passed to PaymentProcessorEncoder would contain the values for the additional fee on top and cosignature validation. Use the PaymentProcessorEncoder to encode the calldata(./calldata-encoding#encodebuylistingcalldata). *Note: The taker/buyer (`msg.sender`) pays for the item and any fee on top when applicable. Because the `buyListing` function is payable, both native or ERC-20 payment methods are accepted.* ### Accept Offer Exchanges should call `acceptOffer` when a taker wants to sell a single item that matches an offer made by a prospective buyer. The kinds of offers are currently supported by Payment Processor: 1. Item Offer - An offer made on a specific collection where only one specific token id can be used to fill the order. 2. Collection Offer - An offer made on a specific collection where any token id can be used to fill the order. 3. Token Set Offer - An offer made on a specific collection where any token id contained in a specified subset of token ids can be used to fill the order. There are four order types that may be filled through the `acceptOffer` function: **Standard Offer *Without* Fee On Top** - To be used when the taker fills a standard buy order (offer) and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `cosignature` and `feeOnTop` struct parameters passed to PaymentProcessorEncoder would be filled with zero values. **Standard Offer *With* Fee On Top** - To be used when the taker fills a standard buy order (offer) via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution the `cosignature` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `feeOnTop` struct parameter would contain the `recipient` and `amount` for the fee being added by the taker marketplace. **Cosigned Offer *Without* Fee On Top** - To be used when the taker fills a co-signed buy order (offer) and the maker and taker marketplace is the same, or when the taker marketplace does not include a fee on top. For this type of order execution the `feeOnTop` struct parameter passed to PaymentProcessorEncoder would be filled with zero values and the `cosignature` struct parameter would contain the `signer`, `taker`, `expiration` and cosignature `v`, `r` and `s` values for the cosignature. **Cosigned Offer *With* Fee On Top** - To be used when the taker fills a co-signed buy order (offer) via a secondary marketplace. While the maker marketplace can assess a primary fee that is deducted from the seller's proceeds, the taker marketplace may assess an extra `fee on top` paid by the taker in excess of the item purchase price. For this type of order execution both the `feeOnTop` and `cosignature` struct parameters passed to PaymentProcessorEncoder would contain the values for the additional fee on top and cosignature validation. *Note: The taker/seller (`msg.sender`) pays for the fee on top when applicable. The maker/buyer pays the cost of the filled item. Because the `acceptOffer` function is not payable, only ERC-20 payment methods are accepted.* ### Bulk Buy Listings Exchanges should call `bulkBuyListings` when a taker wants to purchase more than one listing using a UX analagous to a "Shopping Cart". This allows a taker to select many NFTs across different collections or with varying payment methods and fill all of the listings at once. Each listing being purchased may be any of the four transaction types supported by the `buyListing` function (see [Buy Listing](#buy-listing) above). When encoding bulk purchases the `Order`, `SignatureECDSA`, `Cosignature` and `FeeOnTop` struct values are passed as arrays of values where the item at the same index in each array corresponds to the listing being purchased. All four arrays **MUST** be of equal length and follow the same structure as a single listing purchase for standard versus cosigned orders and execution with or without a fee on top. ### Bulk Accept Offers Exchanges should call `bulkAcceptOffers` when a taker wants to accept/fill more than one offer at once. This allows a taker to sell multiple items they own across one or more collections in a single transaction. The kinds of offers are currently supported by Payment Processor: 1. Item Offer - An offer made on a specific collection where only one specific token id can be used to fill the order. 2. Collection Offer - An offer made on a specific collection where any token id can be used to fill the order. 3. Token Set Offer - An offer made on a specific collection where any token id contained in a specified subset of token ids can be used to fill the order. Each offer being accepted may be any of the four transaction types supported by the `acceptOffer` function (see [Accept Offer](#accept-offer) above). When encoding bulk accept offers the `isCollectionLevelOffer` boolean value and `Order`, `SignatureECDSA`, `TokenSetProof`, `Cosignature` and `FeeOnTop` struct values are passed as arrays of values where the item at the same index in each array corresponds to the offer being accepted. All six arrays **MUST** be of equal length and follow the same structure as a single offer acceptance for standard versus cosigned orders and execution with or without a fee on top. ### Sweep Collection Exchanges should call `sweepCollection` (a more gas efficient form of `bulkBuyListings`) when a taker wants to purchase more than similar listings. A common UX is the "Collection Sweep" UX where the taker specifies a number of items from a collection they want to buy, with some pricing limits. For a sweep to be filled, all items in the sweep order must share the following commonalities: 1. All sell orders fillable in the sweep must be from the same collection. 3. All sell orders fillable in the sweep must use the same method of payment. 5. All sell orders fillable in the sweep must specify the same beneficiary of the NFT. When filling sweep orders, the following fields can be different for each filled item: 1. Maker 2. Marketplace 3. Fallback Royalty Recipient 4. Token Id 5. Amount 6. Item Price 7. Nonce (for standard orders) 8. Expiration 9. Marketplace Fee Numerator 10. Max Royalty Fee Numerator Sweep orders may be executed *with* or *without* a single fee on top for the sweep. To execute *with* a fee on top the `feeOnTop` struct value passed to PaymentProcessorEncoder would contain the `recipient` and `amount` for the fee. To execute *without* a fee on top the `feeOnTop` struct value passed to PaymentProcessorEncoder would contain zero values for `recipient` and `amount`. When encoding sweep orders with PaymentProcessorEncoder the `SweepItem`, `SignatureECDSA` and `Cosignature` structs are passed as arrays of values where the item at the same index in each array corresponds to the listing being purchased. All three arrays **MUST** be of equal length and follow the same structure as a single listing purchase for standard versus cosigned orders. *Note: The taker/buyer (`msg.sender`) pays for the items and any fee on top when applicable. Because the `sweepCollection` function is payable, both native or ERC-20 payment methods are accepted.* *Note: For the most gas-efficient collection sweeps, marketplaces should make a best effort to group orders in the array by marketplace address, seller, and royalty recipient.* ### Cosignature Format All cosigned listings and offers require a secondary signature to be provided at fill-time/execution time. The same cosignature format applies to all cosigned maker signatures, and takes a cosigner expiration and the maker's signature v, r, s components as typed data inputs. Note: for security purposes, co-signatures must always be signed by EOA co-signers. Furthermore, cosignature expiration times should be relatively short. A cosignature expiration time between 5 and 10 minutes is suggested. If the expiration time is too short, transactions may fail because the expiration time elapses before a transaction has been confirmed. However, if the expiration time is too long a leak of the co-signature could be executed well into the future. ```js Cosignature( uint8 v, bytes32 r, bytes32 s, uint256 expiration, address taker ) ``` # Cancelling Orders ## Cancelling A Single Order 1. Display maker's orders (listings and/or offers). 2. Maker selects an order to cancel. 3. If standard order, maker confirms transaction for `revokeSingleNonce` (when order type is fill or kill) or `revokeOrderDigest` (when order type is partially fillable). Revoking a single nonce or order digest requires calldata to be encoded with PaymentProcessorEncoder. See [encodeRevokeSingleNonceCalldata](./calldata-encoding#encoderevokesinglenoncecalldata) for `revokeSingleNonce` or [encodeRevokeOrderDigestCalldata](./calldata-encoding#encoderevokeorderdigestcalldata) for `revokeOrderDigest`. 4. If co-signed order flag order as cancelled in DB and do not allow co-signing service to co-sign the order if requested. Order should be hidden from future listing/offer queries. ## Cancelling All Outstanding Standard Orders 1. If the maker chooses to cancel ALL outstanding standard listings and offers they have made, the maker simply needs to confirm a `revokeMasterNonce` transaction. This will cancel/invalidate all prior orders that were signed using their previous master nonce. `revokeMasterNonce` has no calldata requirements and may be called directly by a maker without using PaymentProcessorEncoder. # Cancelling All Outstanding Co-Signed Orders 1. The marketplace should flag all outstanding orders for the user as cancelled in their database. The co-signing service should not return co-signatures for any of the cancelled orders, and the orders should be hidden from all future queries. # Cosigner Security Cosignatures on orders allow an order book to offer users the ability to gaslessly cancel their listings and offers. To guard against bad actors order books ***MUST*** keep the private keys of the cosigning accounts secure and ***SHOULD*** use short expirations on cosignatures so that signed but unexecuted cosignatures are unlikely to be used after the maker has cancelled their order off-chain. If the private key to a cosigner account has been exposed the order book ***MUST*** destroy the cosigner through Payment Processor on all chains that it was used as an order cosigner so that it may not be used to fill off-chain cancelled orders. Cosigners cannot be recovered after they have been destroyed on Payment Processor. ## Destroy a Cosigner 1. Use the cosigner private key to sign the message `COSIGNER_SELF_DESTRUCT`. 2. Call `destroyCosigner` on Payment Processor with the cosigner address and message signature. See [encodeDestroyCosigner](./calldata-encoding#encodedestroycosignercalldata). # Encoding Function Call Parameters In order to not overflow smart contract bytecode size limits, Payment Processor is split into smaller library modules. When making calls to Payment Processor, these calls are forwarded to the corresponding module and implementation function using `DELEGATECALL`. For gas efficiency purposes, the function calldata must be passed in as a raw calldata byte array. Payment Processor is deployed with a helper contract called the PaymentProcessorEncoder. It accepts the expected function implementation parameters and encodes them as a single calldata byte array that should be passed into Payment Processor when making calls to fill orders. The subsections below detail each PaymentProcessorEncoder function. ### encodeBuyListingCalldata Used to encode calldata for Payment Processor `buyListing` function. ```solidity function encodeBuyListingCalldata( address paymentProcessorAddress, Order memory saleDetails, SignatureECDSA memory signature, Cosignature memory cosignature, FeeOnTop memory feeOnTop ) external view returns (bytes memory); ``` ### encodeAcceptOfferCalldata Used to encode calldata for Payment Processor `acceptOffer` function. ```solidity function encodeAcceptOfferCalldata( address paymentProcessorAddress, bool isCollectionLevelOffer, Order memory saleDetails, SignatureECDSA memory signature, TokenSetProof memory tokenSetProof, Cosignature memory cosignature, FeeOnTop memory feeOnTop ) external view returns (bytes memory); ``` *Note: When filling an item offer, `isCollectionLevelOffer` MUST be set to `false`, and `tokenSetProof` MUST be set to the empty `TokenSetProof({rootHash: bytes32(0), proof: bytes32[](0)})`. *Note: When filling a collection offer, `isCollectionLevelOffer` MUST be set to `true`, and `tokenSetProof` MUST be set to the empty `TokenSetProof({rootHash: bytes32(0), proof: bytes32[](0)})`. *Note: When filling a token set offer, `isCollectionLevelOffer` MUST be set to `true`, and `tokenSetProof` MUST contain the root hash of the merkle tree signed by the offer maker, and the proof must be the valid merkle proof for the collection and token id being filled. ### encodeBulkBuyListingsCalldata Used to encode calldata for Payment Processor `bulkBuyListings` function. ```solidity function encodeBulkBuyListingsCalldata( address paymentProcessorAddress, Order[] calldata saleDetailsArray, SignatureECDSA[] calldata signatures, Cosignature[] calldata cosignatures, FeeOnTop[] calldata feesOnTop ) external view returns (bytes memory); ``` *Note: The length of the `saleDetailsArray`, `signatures`, `cosignatures`, and `feesOnTop` MUST match and MUST be non-zero. `saleDetailsArray[index]` MUST correspond to `signatures[index]`, `cosignatures[index]`, and `feesOnTop[index]`.* *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: As fees on top may not apply to all orders being filled, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})` for any order that is not subject to a fee on top.* ### encodeBulkAcceptOffersCalldata Used to encode calldata for Payment Processor `bulkAcceptOffers` function. ```solidity function encodeBulkAcceptOffersCalldata( address paymentProcessorAddress, bool[] memory isCollectionLevelOfferArray, Order[] memory saleDetailsArray, SignatureECDSA[] memory signatures, TokenSetProof[] memory tokenSetProofsArray, Cosignature[] memory cosignaturesArray, FeeOnTop[] memory feesOnTopArray ) external view returns (bytes memory); ``` *Note: The length of the `saleDetailsArray`, `signatures`, `tokenSetProofsArray`, `isCollectionLevelOfferArray`, `cosignatures`, and `feesOnTopArray` MUST match and MUST be non-zero. `saleDetailsArray[index]` MUST correspond to `signatures[index]`, `tokenSetProofsArray[index]`, `isCollectionLevelOfferArray[index]`, `cosignatures[index]`, and `feesOnTopArray[index]`.* *Note: When filling an item offer, `isCollectionLevelOfferArray[index]` MUST be set to `false`, and `tokenSetProofsArray[index]` MUST be set to the empty `TokenSetProof({rootHash: bytes32(0), proof: bytes32[](0)})`. *Note: When filling a collection offer, `isCollectionLevelOfferArray[index]` MUST be set to `true`, and `tokenSetProofsArray[index]` MUST be set to the empty `TokenSetProof({rootHash: bytes32(0), proof: bytes32[](0)})`. *Note: When filling a token set offer, `isCollectionLevelOfferArray[index]` MUST be set to `true`, and `tokenSetProofsArray[index]` MUST contain the root hash of the merkle tree signed by the offer maker, and the proof must be the valid merkle proof for the collection and token id being filled. *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: As fees on top may not apply to all orders being filled, `feesOnTop[index]` MUST be set to the empty `FeeOnTop({recipient: address(0), amount: 0})` for any order that is not subject to a fee on top.* ### encodeSweepCollectionCalldata Used to encode calldata for Payment Processor `sweepCollection` function. ```solidity function encodeSweepCollectionCalldata( address paymentProcessorAddress, FeeOnTop memory feeOnTop, SweepOrder memory sweepOrder, SweepItem[] calldata items, SignatureECDSA[] calldata signatures, Cosignature[] calldata cosignatures ) external view returns (bytes memory); ``` *Note: The length of the `items`, `signatures`, and `cosignatures` MUST match and MUST be non-zero. `items[index]` MUST correspond to `signatures[index]` and `cosignatures[index]`.* *Note: As co-signatures may not apply to all orders being filled, `cosignatures[index]` MUST be set to the empty `Cosignature({signer: address(0), taker: address(0), expiration: 0, v: 0, r: bytes32(0), s: bytes32(0)})` for any order that is not co-signed.* *Note: A single fee on top may be applied to the entire sweep order.* ### encodeRevokeSingleNonceCalldata Used to encode calldata for Payment Processor `revokeSingleNonce` function (on-chain order cancellation). ```solidity function encodeRevokeSingleNonceCalldata(address paymentProcessorAddress, uint256 nonce) external view returns (bytes memory); ``` ### encodeRevokeOrderDigestCalldata Used to encode calldata for Payment Processor `revokeOrderDigest` function (on-chain order cancellation). ```solidity function encodeRevokeOrderDigestCalldata(address paymentProcessorAddress, bytes32 digest) external view returns (bytes memory); ``` ### encodeDestroyCosignerCalldata Used to encode calldata for Payment Processor `destroyCosignerCalldata` function. ```solidity function encodeDestroyCosignerCalldata( address paymentProcessorAddress, address cosigner, SignatureECDSA memory signature ) external view returns (bytes memory); ``` # Nonces Payment Processor uses two types of nonces to manage orders - master nonces and order nonces. Every order maker on Payment Processor has their own master nonce and set of order nonces. The master nonce defaults to zero for an order maker and allows the maker to cancel all open orders that were signed using that master nonce by calling revokeMasterNonce on Payment Processor. revokeMasterNonce will increment the maker's master nonce by one and emit a MasterNonceInvalidated event with the order maker's address and the master nonce that was revoked, the maker's new master nonce will be the revoked nonce plus one. Master nonce is part of an order signature and will result in all previously signed orders with that nonce to recover as a different signer and revert. An order nonce is a 32 byte integer that is used to prevent the replay of orders on Payment Processor and allow for orders to be explicitly cancelled prior to fulfillment by the order maker. When a nonce is consumed by fulfillment or cancellation it cannot be used again. ## How are nonces stored? Order nonces are stored in a bitmap data structure for each order maker where each storage slot is a "bucket" of 256 nonces where nonces 0-255 are in the first bucket, 256-511 are in the second bucket, and so forth. Nonces are packed this way for efficient use of storage within the EVM and to reduce gas costs on transaction execution. At a high level - consuming the first nonce in a bucket costs 20,000 gas units, each additional nonce consumed from the same bucket in the same transaction costs 100 gas units (a 99.5% savings over one slot per nonce or using nonces from different buckets), and an additional nonce consumed from a bucket that previously had a nonce consumed but in a different transaction costs 5,000 gas units (75% saving over one slot per nonce or using nonces from different buckets). Payment Processor does not account for the order book/marketplace in nonce validation - order books must generate nonces for users that will not overlap with nonces created by another order book to prevent fulfilled orders on one order book from interfering with open orders on the other. ## What is an ideal nonce? An ideal nonce for a Payment Processor order will include as many zero bytes in the 32 byte nonce as possible to save calldata gas cost (a zero byte in calldata costs 4 gas units while a non-zero byte costs 16 gas units), pack as many fillable order nonces into a 256 nonce "bucket" as possible, and not overlap with nonces used by other order books. To meet these objectives of an ideal nonce we recommend using the upper 4 bytes of the 32 byte nonce value as a marketplace differentiator and incrementing the nonce used for order signing by one for each marketplace as a user signs an order as a simple method for generating an order nonce. ### Example ```js maker = "0x1234...FFFF" orderbookDifferentiator = keccak256("YOUR_ORDER_BOOK") << 224 // 0x3cd2fd820000...0000 orderMakerNextOrderbookNonce = getNextNonce(orderbookDifferentiator, maker) // query database for next nonce, for example assume this value is 123 orderNonce = orderbookDifferentiator | orderMakerNextOrderbookNonce // 0x3cd2fd820000..007B ``` A more advanced solution could track prior nonces used for order signing where another factor on the order, such as expiration time or master nonce revocation, has made a prior order unfillable and re-use those nonces to better pack fulfilled nonces. # Contract Deployments #### Deployed Chains: - [Ethereum Mainnet](#ethereum) - [Base Mainnet](#base) - [Optimism Mainnet](#optimism) - [Arbitrum One Mainnet](#arbitrum) - [Polygon Mainnet](#polygon) - [Polygon zkEVM Mainnet](#polygon-zkevm) - [Avalanche C-Chain Mainnet](#avalanche-c-chain) - [Binance Smart Chain (BSC) Mainnet](#binance-smart-chain) - [Ethereum Sepolia Testnet](#ethereum-sepolia) #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Payment Processor V2 | `0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834` | | Payment Processor V2 Encoder | `0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC` | | Payment Processor V2 Configuration | `0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008` | | Payment Processor Module On Chain Cancellation | `0x9A1D00fcC838601680c731640A070e56fA605c81` | | Payment Processor Module Payment Settings | `0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA` | | Payment Processor Module Trades | `0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc` | | Payment Processor Module Advanced Trades | `0x9A1D00899099D06fe50FB31F03Db5345c45ABb36` | ## Mainnet Deployments ### Ethereum - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://etherscan.io/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://etherscan.io/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://etherscan.io/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://etherscan.io/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://etherscan.io/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://etherscan.io/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://etherscan.io/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Base - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://basescan.org/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://basescan.org/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://basescan.org/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://basescan.org/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://basescan.org/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://basescan.org/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://basescan.org/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Optimism - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://optimistic.etherscan.io/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://optimistic.etherscan.io/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://optimistic.etherscan.io/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://optimistic.etherscan.io/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://optimistic.etherscan.io/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://optimistic.etherscan.io/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://optimistic.etherscan.io/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Arbitrum - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://arbiscan.io/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://arbiscan.io/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://arbiscan.io/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://arbiscan.io/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://arbiscan.io/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://arbiscan.io/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://arbiscan.io/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Polygon - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://polygonscan.com/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://polygonscan.com/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://polygonscan.com/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://polygonscan.com/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://polygonscan.com/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://polygonscan.com/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://polygonscan.com/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Polygon zkEVM - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://zkevm.polygonscan.com/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://zkevm.polygonscan.com/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://zkevm.polygonscan.com/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://zkevm.polygonscan.com/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://zkevm.polygonscan.com/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://zkevm.polygonscan.com/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://zkevm.polygonscan.com/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Binance Smart Chain - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://bscscan.com/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://bscscan.com/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://bscscan.com/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://bscscan.com/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://bscscan.com/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://bscscan.com/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://bscscan.com/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ### Avalanche C-Chain - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://snowtrace.io/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://snowtrace.io/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://snowtrace.io/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://snowtrace.io/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://snowtrace.io/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://snowtrace.io/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://snowtrace.io/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) ## Testnet Deployments ### Ethereum Sepolia - Payment Processor V2 - [0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834](https://sepolia.etherscan.io/address/0x9A1D00bEd7CD04BCDA516d721A596eb22Aac6834) - Payment Processor V2 Encoder - [0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC](https://sepolia.etherscan.io/address/0x9A1D005743777cbc7e9ae7eC75BCb9b314aaeFbC) - Payment Processor V2 Configuration - [0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008](https://sepolia.etherscan.io/address/0x9A1D00Bc981DA5cea8300a999c0d15E2f7F03008) - Payment Processor Module On Chain Cancellation - [0x9A1D00fcC838601680c731640A070e56fA605c81](https://sepolia.etherscan.io/address/0x9A1D00fcC838601680c731640A070e56fA605c81) - Payment Processor Module Payment Settings - [0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA](https://sepolia.etherscan.io/address/0x9A1D007a3E9d558a6dAbF413a34810E85834e3BA) - Payment Processor Module Trades - [0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc](https://sepolia.etherscan.io/address/0x9A1D0059f5534E7a6C6c4DAe390ebD3a731Bd7Dc) - Payment Processor Module Advanced Trades - [0x9A1D00899099D06fe50FB31F03Db5345c45ABb36](https://sepolia.etherscan.io/address/0x9A1D00899099D06fe50FB31F03Db5345c45ABb36) # TokenMaster # Introduction TokenMaster is a next-generation fungible token protocol optimized for onchain consumer economies. Built on the advanced ERC20-C standard, TokenMaster enables the creation of highly customizable tokens tailored for diverse applications. Tokens launched via TokenMaster are paired with an existing token (e.g., the chain's native token or another ERC20), functioning as both a pool and a token. Creators can select from multiple pool types to tailor the behavior of their token to their application's needs. # Features: - **Token Deployment:** Deploy ERC20-C tokens paired with an existing token to establish market value. - **Flexible Transactions:** Enable buy, sell, and ***spend*** operations with robust event hooks for seamless integration across onchain and offchain systems. - **Creator Empowerment:** Fine-grained creator controls to define tokenomics and ecosystem behavior, free from external constraints. - **User Safety:** Built-in guardrails to ensure secure and user-friendly token interactions. - **Future-Ready Architecture:** Modular and extensible design, enabling new token mechanics and protocol wrapping for enhanced customizability. # Benefits: * Near-zero cost to bootstrap a token. * Tokens scale to market demand without artificial limitations. * Multiple sources of revenue for creators while maintaining price stability. # Definitions - **Apptoken:** - A programmable token created for use within a specific application or ecosystem. - **Advanced Order:** - A signed instruction enabling advanced buy, sell, or spend transactions. These orders allow creators to integrate onchain hooks and optional offchain actions seamlessly. - **Cosigner**: An optional signer in advanced orders that adds an additional layer of permissioning, allowing creators to restrict execution to specific executors. - **Hook Extra Data**: Arbitrary bytes data passed to hooks during transactions, allowing for custom logic execution tailored to specific use cases. - **Oracle Extra Data**: Arbitrary bytes data passed to oracles during transactions, providing context or parameters for dynamic value adjustments. - **Pairing Restrictions**: Rules defined by the creator to control which tokens can be paired with a TokenMaster token, ensuring compatibility and avoiding unintentional pairings. - **PermitC**: A permit-based system allowing token transfers using advanced logic for expiration and bulk cancellation of open approvals, leveraging EIP-712 signatures for secure and efficient interactions. - **TokenMaster Router:** The core contract that facilitates token deployment, enforces pairing rules, processes transactions (including advanced orders), and triggers onchain hooks for extended functionality. - **Trusted Channels:** - Pre-approved addresses authorized to initiate transactions through the TokenMaster Router for specific tokens. # TokenMaster Router TokenMaster Router serves as the primary hub for all TokenMaster transactions allowing creators to control all aspects of their token lifecycle. TokenMaster Router implements the Limit Break TrustedForwarder system to allow creators and protocol integrators to route transactions through an external entry point, that may be permissioned with a signing authority for offchain authorization, as well as a `Trusted Channels` list that restricts the caller address to being in a list specified by the creator. `Trusted Channels` may be used to require a specific TrustedForwarder or a custom wrapper that adds logic to TokenMaster transactions. # Token Deployment Flow TokenMaster allows creators to deploy tokens with precise configurations tailored to their application's needs. The deployment process involves selecting the desired token type, configuring deployment parameters, and leveraging deterministic address computation for predictability. ## Steps to Deploy a Token 1. **Choose Token Type**: - Decide the type of pool (e.g., Standard, Stable, Promotional) that best suits your application's requirements. 2. **Construct Deployment Parameters**: - Use the following key structures to define the token's behavior and settings: - [**DeploymentParameters**](./datatypes#deploymentparameters): General deployment settings such as the token factory, deterministic address salt, and transaction rules. - [**PoolDeploymentParameters**](./datatypes#pooldeploymentparameters): Pool-specific settings, including token metadata, pairing token, and initial supply. 3. **Compute the Deterministic Deployment Address**: - Use the factory contract's helper function `computeDeploymentAddress` to calculate the token's address before deployment: ```solidity function computeDeploymentAddress( bytes32 tokenSalt, PoolDeploymentParameters calldata poolParams, uint256 pairedValueIn, uint256 infrastructureFeeBPS ) external view returns(address deploymentAddress); ``` - This ensures predictability and prevents address conflicts. 4. **Deploy the Token**: - Call the `deployToken` function on the TokenMaster Router with the constructed parameters: ```solidity function deployToken( DeploymentParameters calldata deploymentParameters, SignatureECDSA calldata signature ) external payable; ``` - Note: The `signature` parameter is only required when TokenMaster Router has a signing authority enabled for token deployments. If a signing authority is not configured, the `signature` parameter is validated. 5. **Token Activation**: - Upon successful deployment, the token will be fully operational and managed via the TokenMaster Router. # Transactions ## Buy Transaction Flow 1. **Specify Purchase Amount** The buyer inputs the desired number of tokens. 2. **Calculate Cost** The platform computes the expected cost in the paired token, accounting for slippage (refer to [TokenMaster Pools](../Pools/tokenmaster-pools) for cost calculations by pool type). 3. **Approval Handling (ERC20)** - **Using PermitC**: 1. Verify user’s token approval for PermitC. If insufficient, prompt an approval transaction. 2. Generate a signed PermitC transfer order. See [PermitTransfer](./advanced-orders#permit-transfer). - **Using Direct Approvals**: - Verify user’s token approval for TokenMaster Router. If insufficient, prompt an approval transaction. 4. **Cosignature (Optional)** Obtain cosignature if required by the advanced order. See [Cosigning](./advanced-orders#cosigning). 5. **Transaction Execution** - **Advanced Buy or PermitC Orders**: - Call `buyTokensAdvanced` with the required parameters ([BuyOrder](./datatypes#buyorder), [SignedOrder](./datatypes#signedorder), and [PermitTransfer](./datatypes#permittransfer)). - Set `signedOrder.hook` to `address(0)` for basic orders. - Set `permitTransfer.permitProcessor` to `address(0)` for direct approvals. - **Basic Buy Orders**: - Call `buyTokens` with the [BuyOrder](./datatypes#buyorder). - **Native Token Pairing**: - Include the native token value as `msg.value` when invoking the router. --- ## Sell Transaction Flow 1. **Specify Sale Amount** The user inputs the desired number of tokens to sell. 2. **Calculate Expected Value** The platform computes the expected value in the paired token, accounting for slippage (refer to [TokenMaster Pools](../Pools/tokenmaster-pools) for sell value calculations by pool type). 3. **Cosignature (Optional)** Obtain cosignature if the advanced order requires it. See [Cosigning](./advanced-orders#cosigning). 4. **Transaction Execution** - **Advanced Sell Orders**: - Call `sellTokensAdvanced` with the [SellOrder](./datatypes#sellorder) and [SignedOrder](./datatypes#signedorder). - **Basic Sell Orders**: - Call `sellTokens` with the [SellOrder](./datatypes#sellorder). --- ## Spend Transaction Flow 1. **Select Spend Order** The user selects a signed spend order to execute. 2. **Specify Multiples** The user inputs the number of multiples of the spend order to execute. 3. **Calculate Maximum Spend** The platform determines the maximum spend amount based on the signed spend order and the specified multiples, adjusting for value and slippage if an oracle price adjustment is involved. 4. **Cosignature (Optional)** Obtain cosignature if the advanced order requires it. See [Cosigning](./advanced-orders#cosigning). 5. **Transaction Execution** - Call `spendTokens` with the [SpendOrder](./datatypes#spendorder) and [SignedOrder](./datatypes#signedorder). --- ### Example Use Cases - **Game Development** A game developer could use spends to sell items and lives to players using event indexing to issue the items offchain. - **NFT Minting** A project could use spends with onchain hooks to mint NFTs using their ecosystem apptoken. - **Retail Payments** A retail point-of-sale system could generate signed orders for customers to pay onchain using the store's token. # Advanced Order Signing TokenMaster enables creators to enhance user interactions by supporting optional signed advanced orders for buy and sell transactions in addition to spend orders. These advanced orders allow creators to integrate onchain [hooks](./hooks) and provide incentives or custom logic. ## Features of Advanced Orders: - **Custom Incentives**: Creators can reward buyers and sellers through advanced order mechanisms. - **Spend Transactions**: Require signed orders but allow hooks to be optional. Offchain monitoring of the [SpendOrderFilled](./events#order-fill-events) event can substitute for hooks. ### Advanced Buy Order EIP-712 Primary Message Type: ```solidity BuyTokenMasterToken(bytes32 creatorBuyIdentifier,address tokenMasterToken,address tokenMasterOracle,address baseToken,uint256 baseValue,uint256 maxPerWallet,uint256 maxTotal,uint256 expiration,address hook,address cosigner) ``` ### Advanced Sell Order EIP-712 Primary Message Type: ```solidity SellTokenMasterToken(bytes32 creatorSellIdentifier,address tokenMasterToken,address tokenMasterOracle,address baseToken,uint256 baseValue,uint256 maxPerWallet,uint256 maxTotal,uint256 expiration,address hook,address cosigner) ``` ### Spend Order EIP-712 Primary Message Type: ```solidity SpendTokenMasterToken(bytes32 creatorSpendIdentifier,address tokenMasterToken,address tokenMasterOracle,address baseToken,uint256 baseValue,uint256 maxPerWallet,uint256 maxTotal,uint256 expiration,address hook,address cosigner) ``` * `creator*Identifier` is a value specified by the creator to identify the order during onchain hook execution or for offchain processing. * `tokenMasterOracle` must be set to the zero address if not using an oracle to adjust `baseValue`. * `baseToken` is only applicable to orders that are using an oracle to adjust the `baseValue`. * `baseValue` is the amount of token value required to execute the order. * `maxPerWallet` is the maximum amount that one wallet can execute against the specific signed order (Note for clarity: a specific signed order encompasses all fields in the message type, not just the creator identifier). For spends this amount is specified in multiples, for buys and sells this amount is specified in tokens. * `maxTotal` is the maximum amount that all wallets can execute against the specific signed order (Note for clarity: a specific signed order encompasses all fields in the message type, not just the creator identifier). For spends this amount is specified in multiples, for buys and sells this amount is specified in tokens. * `expiration` is the Unix timestamp after which the signed order is invalid. * `hook` is the onchain hook to call after the buy/sell/spend is executed. A hook is required for buy and sell advanced orders but optional for spends. * `cosigner` is the address of the cosigner for the signed order (see [Cosigning](#cosigning)). ## EIP-712 Signature Requirements: - Advanced orders must be signed by an authorized account designated on the TokenMaster Router through the [setOrderSigner](./functions#token-deployment--settings-functions) function. - **Domain Separator**: - **Name**: `TokenMasterRouter` - **Version**: `1` - **Chain ID**: The chain where the order is executed. - **Verifying Contract**: Address of TokenMaster Router. Refer to the [SignedOrder](./datatypes#signedorder) struct for formatting advanced order parameters and signatures. ## Cosigning Advanced orders can include an optional cosigner to add an extra layer of permissioning. Cosigners enable creators to enforce execution restrictions, such as limiting specific transactions to authorized executors. ### Key Features: - **Permissioning**: Restrict advanced orders to pre-approved executors. - **EIP-712 Cosignature**: - Cosignatures include the `v`, `r`, and `s` components of the signed advanced order. - These must match the cosigner specified in the [Cosignature](./datatypes#cosignature) struct. ### Cosignature Format: Cosignature EIP-712 Primary Message Type: ```solidity Cosignature(uint8 v, bytes32 r, bytes32 s, uint256 expiration, address executor) ``` ### Developer Notes: - Cosigner addresses are not permissioned on TokenMaster Router the only requirement is that when a non-zero cosigner address is specified in the [Cosignature](./datatypes#cosignature) struct the cosignature's signature recovers to that address. ### Example Use Cases: - Require a transaction is originating from within a creator's application. - Require a user has completed certain actions prior to executing a transaction. ## Permit Transfer TokenMaster supports PermitC permit transfers for paired tokens when buying TokenMaster tokens. To execute a permit transfer the platform *must* get a signed permit from the user and ensure the PermitC contract being utilized has sufficient allowance from the user for the token being permitted. The domain separator for the permit will be the domain separator of the PermitC contract being utilized. Permit Transfer EIP-712 Primary Message Type: ```solidity PermitTransferFromWithAdditionalData(uint256 tokenType,address token,uint256 id,uint256 amount,uint256 nonce,address operator,uint256 expiration,uint256 masterNonce,AdvancedBuyOrder advancedBuyOrder)AdvancedBuyOrder(address tokenMasterToken,uint256 tokensToBuy,uint256 pairedValueIn,bytes32 creatorBuyIdentifier,address hook,uint8 buyOrderSignatureV,bytes32 buyOrderSignatureR,bytes32 buyOrderSignatureS) ``` # Hooks Hooks are custom smart contracts that execute additional onchain actions after advanced orders are processed. Creators can deploy hooks to extend functionality and integrate with their ecosystem. ## Supported Hook Types: - **Buy Order Hooks**: Triggered after advanced buy orders. - **Implementation Requirement**: Must implement [ITokenMasterBuyHook](https://github.com/limitbreakinc/tm-tokenmaster-examples/tree/main/lib/tm-tokenmaster/src/interfaces/ITokenMasterBuyHook.sol). ```solidity //SPDX-License-Identifier: LicenseRef-PolyForm-Strict-1.0.0 pragma solidity 0.8.24; /** * @title ITokenMasterBuyHook * @author Limit Break, Inc. * @notice Interface that must be implemented by contracts acting as a buy hook * @notice for advanced buy orders. */ interface ITokenMasterBuyHook { function tokenMasterBuyHook( address tokenMasterToken, address buyer, bytes32 creatorBuyIdentifier, uint256 amountPurchased, bytes calldata hookExtraData ) external; } ``` - **Sell Order Hooks**: Triggered after advanced sell orders. - **Implementation Requirement**: Must implement [ITokenMasterSellHook](https://github.com/limitbreakinc/tm-tokenmaster-examples/tree/main/lib/tm-tokenmaster/src/interfaces/ITokenMasterSellHook.sol). ```solidity //SPDX-License-Identifier: LicenseRef-PolyForm-Strict-1.0.0 pragma solidity 0.8.24; /** * @title ITokenMasterSellHook * @author Limit Break, Inc. * @notice Interface that must be implemented by contracts acting as a sell hook * @notice for advanced sell orders. */ interface ITokenMasterSellHook { function tokenMasterSellHook( address tokenMasterToken, address seller, bytes32 creatorSellIdentifier, uint256 amountSold, bytes calldata hookExtraData ) external; } ``` - **Spend Order Hooks**: Triggered after spend orders. - **Implementation Requirement**: Must implement [ITokenMasterSpendHook](https://github.com/limitbreakinc/tm-tokenmaster-examples/tree/main/lib/tm-tokenmaster/src/interfaces/ITokenMasterSpendHook.sol). ```solidity //SPDX-License-Identifier: LicenseRef-PolyForm-Strict-1.0.0 pragma solidity 0.8.24; /** * @title ITokenMasterSpendHook * @author Limit Break, Inc. * @notice Interface that must be implemented by contracts acting as a spend hook * @notice for spend orders. */ interface ITokenMasterSpendHook { function tokenMasterSpendHook( address tokenMasterToken, address spender, bytes32 creatorSpendIdentifier, uint256 multiplier, bytes calldata hookExtraData ) external; } ``` ## Key Parameters: - **Transaction Context**: Hooks receive details such as token being transacted, executor address, creator identifier, and quantity transacted. Quantity transacted is amount of token for buy and sells, multiples for spends. ## Hook Design Notes: - **Validation**: Hook contracts must validate the caller as the TokenMaster Router and confirm the token involved is valid for the hook's logic. - **Extra Data**: Hooks can receive additional contextual data through the `extraData` parameter for custom use cases. Validation of this data is the responsibility of the hook logic. # Oracles Oracles in TokenMaster allow dynamic adjustment of transaction values by referencing external data, such as market conditions or token price feeds. They enable greater flexibility in token mechanics and creator-defined customizations. ## Key Features: - **Dynamic Adjustments**: Modify transaction requirements (e.g., token value or spend amount) based on external inputs. - **Transaction Types Supported**: - **Buy Orders**: Specify the minimum required paired token value for token purchases. - **Sell Orders**: Determine the paired token value received for token sales. - **Spend Orders**: Define the per-multiple spend amount in paired tokens. ## Oracle Parameters and Flow: ### Key Parameters: - **Base Value**: The required token value for the transaction. If no oracle is used, `tokenMasterOracle` is set to `address(0)` in the `SignedOrder`. - **Extra Data**: Optional additional information passed to the oracle via the `extraData` field for advanced logic. ### Transaction Type Values: | Transaction | Type Value | | ----------- | ---------- | | Buy | `0` | | Sell | `1` | | Spend | `2` | ## Example Use Cases: 1. **Price Pegging**: Use oracles to dynamically adjust token requirements to maintain a stable price (e.g., pegged to a stablecoin). 2. **Discounts and Promotions**: Apply promotional rates or discounts by dynamically altering the required token value based on demand or time-limited campaigns. ## Developer Notes: - **Implementation Requirement**: Must implement [ITokenMasterOracle](https://github.com/limitbreakinc/tm-tokenmaster-examples/tree/main/lib/tm-tokenmaster/src/interfaces/ITokenMasterOracle.sol). ```solidity //SPDX-License-Identifier: LicenseRef-PolyForm-Strict-1.0.0 pragma solidity 0.8.24; /** * @title ITokenMasterOracle * @author Limit Break, Inc. * @notice Interface that must be implemented by contracts acting as an oracle * @notice for advanced orders. */ interface ITokenMasterOracle { function adjustValue( uint256 transactionType, address executor, address tokenMasterToken, address baseToken, uint256 baseValue, bytes calldata oracleExtraData ) external view returns(uint256 tokenValue); } ``` - **Data Decoding**: Decoding and validating the `extraData` field is the responsibility of the oracle logic. # Data Types ## DeploymentParameters This struct defines parameters used by the TokenMasterRouter and factories for deploying tokens. - **tokenFactory**: The token factory to use to deploy a specific pool type. - **tokenSalt**: The salt value to use when deploying the token to control the deterministic address. - **tokenAddress**: The deterministic address of the token that will be deployed. - **blockTransactionsFromUntrustedChannels**: Initial setting for blocking transactions from untrusted channels. - **restrictPairingToLists**: Initial setting for restricting pairing of the new token with other tokens. - **poolParams**: The parameters that will be sent during token contract construction. - **maxInfrastructureFeeBPS**: The maximum infrastructure fee that is allowed without reverting the deployment. ```solidity struct DeploymentParameters { address tokenFactory; bytes32 tokenSalt; address tokenAddress; bool blockTransactionsFromUntrustedChannels; bool restrictPairingToLists; PoolDeploymentParameters poolParams; uint16 maxInfrastructureFeeBPS; } ``` ## PoolDeploymentParameters This struct defines parameters that are sent by token factories to create a token contract. - **name**: The name of the token. - **symbol**: The symbol of the token. - **decimals**: The number of decimals of the token. - **initialOwner**: Address to set as the initial owner of the token. - **pairedToken**: Address of the token to pair with the new token, for native token use `address(0)`. - **initialPairedTokenToDeposit**: Amount of paired token to deposit to the new token pool. - **encodedInitializationArgs**: Bytes array of ABI encoded initialization arguments to allow new pool types with different types of constructor arguments that are decoded during deployment. - **defaultTransferValidator**: Address of the initial transfer validator for a token. - **useRouterForPairedTransfers**: If true, the pool will default to allowing the router to transfer paired tokens during operations that require the paired token to transfer from the pool. This is useful when pairing with ERC20C tokens that utilize the default operator whitelist which includes the TokenMasterRouter but does not include individual token pools. - **partnerFeeRecipient**: The address that will receive partner fee shares. - **partnerFeeBPS**: The fee rate in BPS for partner fees. ```solidity struct PoolDeploymentParameters { string name; string symbol; uint8 tokenDecimals; address initialOwner; address pairedToken; uint256 initialPairedTokenToDeposit; bytes encodedInitializationArgs; address defaultTransferValidator; bool useRouterForPairedTransfers; address partnerFeeRecipient; uint256 partnerFeeBPS; } ``` ## BuyOrder This struct defines buy order base parameters. - **tokenMasterToken**: The address of the TokenMaster token to buy. - **tokensToBuy**: The amount of tokens to buy. - **pairedValueIn**: The amount of paired tokens to transfer in to the token contract for the purchase. ```solidity struct BuyOrder { address tokenMasterToken; uint256 tokensToBuy; uint256 pairedValueIn; } ``` ## PermitTransfer This struct defines a permit transfer parameters used in advanced buy orders that utilize PermitC advanced permit transfer functionality. - **permitProcessor**: The address of the PermitC-compliant permit processor to use for the transfer. - **nonce**: The permit nonce to use for the permit transfer signature validation. - **permitAmount**: The amount that the permit was signed for. - **expiration**: The time, in seconds since the Unix epoch, that the permit will expire. - **signedPermit**: The permit signature bytes authorizing the transfer. ```solidity struct PermitTransfer { address permitProcessor; uint256 nonce; uint256 permitAmount; uint256 expiration; bytes signedPermit; } ``` ## SellOrder This struct defines sell order base parameters. - **tokenMasterToken**: The address of the TokenMaster token to sell. - **tokensToSell**: The amount of tokens to sell. - **minimumOut**: The minimum output of paired tokens to be received by the seller without the transaction reverting. ```solidity struct SellOrder { address tokenMasterToken; uint256 tokensToSell; uint256 minimumOut; } ``` ## SpendOrder This struct defines spend order base parameters. It must be combined with a `SignedOrder` to execute a token spend transaction. - **tokenMasterToken**: The address of the TokenMaster token to spend. - **multiplier**: The multiplier of the signed spend order's `baseValue`, adjusted by an oracle if specified, to be spent. - **maxAmountToSpend**: The maximum amount the spender will spend on the order without the transaction reverting. ```solidity struct SpendOrder { address tokenMasterToken; uint256 multiplier; uint256 maxAmountToSpend; } ``` ## SignedOrder This struct defines advanced order execution parameters for advanced buy orders, advanced sell orders and spend orders. - **creatorIdentifier**: A value specified by the creator to identify the order for any onchain or offchain benefits to the order executor for executing the order. - **tokenMasterOracle**: An address for an onchain oracle that can adjust the `baseValue` for an advanced order. - **baseToken**: An address for a token to base the `baseValue` on when adjusting value through a TokenMaster Oracle. - **baseValue**: The amount of token required for the order to be executed. * If `tokenMasterOracle` is set to `address(0)`, the `baseToken` will not be utilized and the advanced order will execute with `baseValue` being the amount of the TokenMaster token to be required for the order. - **maxPerWallet**: The maximum amount per wallet that can be executed on the order. For buy and sell advanced orders this amount is in the TokenMaster token amount, for spend orders it is multipliers. - **maxPerWallet**: The maximum amount for all wallets that can be executed on the order. For buy and sell advanced orders this amount is in the TokenMaster token amount, for spend orders it is multipliers. - **expiration**: The time, in seconds since the Unix epoch, that the order will expire. - **hook**: An address for an onchain hook for an order to execute after the buy, sell or spend is executed. - **signature**: The signature from an allowed order signer to authorize the order. - **cosignature**: The cosignature from the cosigner specified by the order signer. - **hookExtraData**: Extra data to send with the call to the onchain hook contract. - **oracleExtraData**: Extra data to send with the call to the oracle contract. ```solidity struct SignedOrder { bytes32 creatorIdentifier; address tokenMasterOracle; address baseToken; uint256 baseValue; uint256 maxPerWallet; uint256 maxTotal; uint256 expiration; address hook; SignatureECDSA signature; Cosignature cosignature; bytes hookExtraData; bytes oracleExtraData; } ``` ## SignatureECDSA The `v`, `r`, and `s` components of an ECDSA signature. For more information [refer to this article](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7). ```solidity struct SignatureECDSA { uint256 v; bytes32 r; bytes32 s; } ``` ## Cosignature This struct defines the cosignature for verifying an order that is a cosigned order. - **signer**: The address that signed the cosigned order. This must match the cosigner that is part of the order signature. * **expiration**: The time, in seconds since the Unix epoch, that the cosignature will expire. * The `v`, `r`, and `s` components of an ECDSA signature. For more information [refer to this article](https://medium.com/mycrypto/the-magic-of-digital-signatures-on-ethereum-98fe184dc9c7). ```solidity struct Cosignature { address signer; uint256 expiration; uint256 v; bytes32 r; bytes32 s; } # Functions ## Order Functions --- Functions related to buying, selling and spending TokenMaster Tokens through the TokenMaster Router. Platforms integrating TokenMaster ***MUST*** implement these functions to allow users to buy, sell and spend tokens through TokenMaster, validate current orders, and allow creators to manage existing orders. ```solidity // Callable by users to buy, sell and spend tokens. function buyTokens(BuyOrder calldata buyOrder) external payable; function buyTokensAdvanced( BuyOrder calldata buyOrder, SignedOrder calldata signedOrder, PermitTransfer calldata permitTransfer ) external payable; function sellTokens(SellOrder calldata sellOrder) external; function sellTokensAdvanced(SellOrder calldata sellOrder, SignedOrder calldata signedOrder) external; function spendTokens( SpendOrder calldata spendOrder, SignedOrder calldata signedOrder ) external; // Retrieves advanced order tracking data for orders with wallet/total limits, validates order and signatures. function getBuyTrackingData( address tokenMasterToken, SignedOrder calldata signedOrder, address buyer ) external view returns ( uint256 totalBought, uint256 totalWalletBought, bool orderDisabled, bool signatureValid, bool cosignatureValid ); function getSellTrackingData( address tokenMasterToken, SignedOrder calldata signedOrder, address seller ) external view returns ( uint256 totalSold, uint256 totalWalletSold, bool orderDisabled, bool signatureValid, bool cosignatureValid ); function getSpendTrackingData( address tokenMasterToken, SignedOrder calldata signedOrder, address spender ) external view returns ( uint256 totalMultipliersSpent, uint256 totalWalletMultipliersSpent, bool orderDisabled, bool signatureValid, bool cosignatureValid ); // Callable by the token owner, default admin role holder, or order manager role holder to disable or re-enable an advanced order. function disableBuyOrder(address tokenMasterToken, SignedOrder calldata signedOrder, bool disabled) external; function disableSellOrder(address tokenMasterToken, SignedOrder calldata signedOrder, bool disabled) external; function disableSpendOrder(address tokenMasterToken, SignedOrder calldata signedOrder, bool disabled) external; ``` ## Token Deployment / Settings Functions Functions related to deploying TokenMaster Tokens, managing and retrieving their current settings. Platforms integrating TokenMaster *must* implement these functions to allow creators to fully manage their tokens. ```solidity // Deploys a TokenMaster Token. function deployToken( DeploymentParameters calldata deploymentParameters, SignatureECDSA calldata signature ) external payable; // Updates token settings on router, may be called by owner or default admin role holder. function updateTokenSettings( address tokenAddress, bool blockTransactionsFromUntrustedChannels, bool restrictPairingToLists ) external; function setOrderSigner(address tokenMasterToken, address signer, bool allowed) external; function setTokenAllowedPairToDeployer(address tokenAddress, address deployer, bool allowed) external; function setTokenAllowedPairToToken(address tokenAddress, address tokenAllowedToPair, bool allowed) external; // Retrieves current settings for tokens. function getTokenSettings( address tokenAddress ) external view returns ( bool deployedByTokenMaster, bool blockTransactionsFromUntrustedChannels, bool restrictPairingToLists, address partnerFeeRecipient ); function getOrderSigners(address tokenMasterToken) external view returns (address[] memory orderSigners); function getTrustedChannels(address tokenMasterToken) external view returns (address[] memory trustedChannels); function getAllowedPairToDeployers(address tokenMasterToken) external view returns (address[] memory allowedPairToDeployers); function getAllowedPairToTokens(address tokenMasterToken) external view returns (address[] memory allowedPairToTokens); ``` ## Creator / Partner / Infrastructure Functions Functions relating to the withdrawal and transfer of creator, partner and infrastructure fees from TokenMaster Tokens. Platforms integrating TokenMaster *MUST* implement the `withdrawCreatorShare` and `transferCreatorShareToMarket` functions for creators to withdraw or manage their earnings. Platforms *should* implement `partnerProposeFeeReceiver`, `acceptProposedPartnerFeeReceiver` and `withdrawFees` to manage partner fee receiver changes and fee withdrawal. Partner fee receivers utilize a two-step transfer process where the current partner fee receiver proposes a new receiving address and the token owner accepts the proposed address. Token owners ***MUST*** inspect the proposed address to ensure it is valid for receiving fees and will not be subject to any transfer restrictions. ```solidity // Callable by the token owner or default admin role holder. function withdrawCreatorShare(ITokenMasterERC20C tokenMasterToken, address withdrawTo, uint256 withdrawAmount) external; function transferCreatorShareToMarket(ITokenMasterERC20C tokenMasterToken, uint256 transferAmount) external; function acceptProposedPartnerFeeReceiver(address tokenMasterToken, address expectedPartnerFeeRecipient) external; // Callable by the partner fee receiver for a token to propose a new fee receiver address. function partnerProposeFeeReceiver(address tokenMasterToken, address proposedPartnerFeeRecipient) external; // Callable by TokenMaster fee collector or a partner for a token. function withdrawFees(ITokenMasterERC20C[] calldata tokenMasterTokens) external; ``` ## Administrative Functions For reference only, these functions are only callable by the TokenMaster admin to update the protocol fee and add/remove allowed token factories. ```solidity // Callable by the TokenMaster admin. function setAllowedTokenFactory(address tokenFactory, bool allowed) external; function setInfrastructureFee(uint16 _infrastructureFeeBPS) external; ``` # Events ## Administrative Events Platforms integrating TokenMaster *should* monitor these event emissions from TokenMaster Router to be aware of infrastructure fee changes and factories being enabled or disabled. ```solidity /// @dev Emitted when the TokenMaster admin updates the infrastructure fee for new token deployments. event InfrastructureFeeUpdated(uint16 infrastructureFeeBPS); /// @dev Emitted when the TokenMaster admin updates an allowed token factory. event AllowedTokenFactoryUpdated(address indexed tokenFactory, bool allowed); ``` --- ## Token Configuration Events Platforms integrating TokenMaster *should* monitor these events from TokenMaster Router for any token that is relevant to their platform. ```solidity /// @dev Emitted when a token has been deployed. event TokenMasterTokenDeployed(address indexed tokenMasterToken, address indexed pairedToken, address indexed tokenFactory); /// @dev Emitted when a token's settings have been updated. event TokenSettingsUpdated( address indexed tokenMasterToken, bool blockTransactionsFromUntrustedChannels, bool restrictPairingToLists ); /// @dev Emitted when a trusted channel has been added or removed. event TrustedChannelUpdated( address indexed tokenAddress, address indexed channel, bool allowed ); /// @dev Emitted when a token's partner has proposed a new fee recipient address. event PartnerFeeRecipientProposed( address indexed tokenAddress, address proposedPartnerFeeRecipient ); /// @dev Emitted when the creator has accepted the token partner's proposed fee recipient address. event PartnerFeeRecipientUpdated( address indexed tokenAddress, address partnerFeeRecipient ); /// @dev Emitted when a deployer has been added or removed as an allowed deployer for tokens pairing to a creator's token. event AllowedPairToDeployersUpdated( address indexed tokenAddress, address indexed deployer, bool allowed ); /// @dev Emitted when a specific token has been added or removed as an allowed token for pairing to a creator's token. event AllowedPairToTokensUpdated( address indexed tokenAddress, address indexed tokenAllowedToPair, bool allowed ); ``` --- ## Order Fill Events Platforms integrating TokenMaster *should* monitor these events from TokenMaster Router for any token that is relevant to their platform. ```solidity /// @dev Emitted when a buy tokens order has been filled. event BuyOrderFilled( address indexed tokenMasterToken, address indexed buyer, uint256 amountPurchased, uint256 totalCost ); /// @dev Emitted when a sell tokens order has been filled. event SellOrderFilled( address indexed tokenMasterToken, address indexed seller, uint256 amountSold, uint256 totalReceived ); /// @dev Emitted when a spend tokens order has been filled. event SpendOrderFilled( address indexed tokenMasterToken, bytes32 indexed creatorSpendIdentifier, address indexed spender, uint256 amountSpent, uint256 multiplier ); ``` --- ## Order Configuration Events Platforms integrating TokenMaster *should* monitor these events from TokenMaster Router for any token that is relevant to their platform. ```solidity /// @dev Emitted when a order signer has been updated. event OrderSignerUpdated( address indexed tokenMasterToken, address indexed signer, bool allowed ); /// @dev Emitted when an advanced buy order has been disabled or enabled. event BuyOrderDisabled( address indexed tokenMasterToken, bytes32 indexed creatorBuyIdentifier, bool disabled ); /// @dev Emitted when an advanced sell order has been disabled or enabled. event SellOrderDisabled( address indexed tokenMasterToken, bytes32 indexed creatorSellIdentifier, bool disabled ); /// @dev Emitted when an spend order has been disabled or enabled. event SpendOrderDisabled( address indexed tokenMasterToken, bytes32 indexed creatorSpendIdentifier, bool disabled ); ``` # TokenMaster Pools TokenMaster has three pool types to choose from: Standard Pool, Stable Pool, and Promotional Pool. ## Common Pool Functionality ### Functions --- The following functions are general to all TokenMaster Tokens. Platforms integrating TokenMaster should use these functions to retrieve pool data and (optionally) to generate administrative transactions to recover funds deposited by user errors. Functions listed below `Calls restricted to only TokenMaster Router` are for reference only and may only be called by the TokenMaster Router contract through its corresponding function to enforce reentrancy guarding, creator control logic, supply relevant parameters that are stored in the router contract, and provide primary or fallback router token transfer support. ```solidity // Permits the token owner to withdraw native tokens or ERC20 tokens that are mistakenly deposited to the pool. function withdrawUnrelatedToken(address tokenAddress, address withdrawTo, uint256 withdrawAmount) external; // Address of the token the TokenMaster Token is paired with. Zero address if paired with chain native token. function PAIRED_TOKEN() external view returns(address); // Current splits of the paired token value by market, creator, infrastructure and partner. function pairedTokenShares() external view returns(uint256 marketShare, uint256 creatorShare, uint256 infrastructureShare, uint256 partnerShare); // Calls restricted to only TokenMaster Router function buyTokens( address buyer, uint256 pairedTokenIn, uint256 pooledTokenToBuy ) external payable returns(uint256 totalCost, uint256 refundByRouterAmount); function sellTokens( address seller, uint256 pooledTokenToSell, uint256 pairedTokenMinimumOut ) external returns (address pairedToken, uint256 pairedValueToSeller, uint256 transferByRouterAmount); function spendTokens(address spender, uint256 pooledTokenToSpend) external; function withdrawCreatorShare( address withdrawTo, uint256 withdrawAmount, address infrastructureFeeRecipient, address partnerFeeRecipient ) external returns ( address pairedToken, uint256 transferByRouterAmountCreator, uint256 transferByRouterAmountInfrastructure, uint256 transferByRouterAmountPartner ); function transferCreatorShareToMarket( uint256 transferAmount, address infrastructureFeeRecipient, address partnerFeeRecipient ) external returns(address pairedToken, uint256 transferByRouterAmountInfrastructure, uint256 transferByRouterAmountPartner); function withdrawFees( address infrastructureFeeRecipient, address partnerFeeRecipient ) external returns ( address pairedToken, uint256 transferByRouterAmountInfrastructure, uint256 transferByRouterAmountPartner ); ``` ### Events --- Platforms integrating TokenMaster *should* index the following events on all tokens that are relevant to the platform to monitor withdrawal/transfer of creator share and fees. ```solidity /// @dev Emitted when a creator withdraws their share or when fees are withdrawn. event CreatorShareWithdrawn(address to, uint256 withdrawAmount, uint256 infrastructureAmount, uint256 partnerAmount); /// @dev Emitted when a creator transfers a portion of their share to the market bonded value. Infrastructure and partner amounts are transferred to their respective receivers. event CreatorShareTransferredToMarket(address to, uint256 transferAmount, uint256 infrastructureAmount, uint256 partnerAmount); ``` ### Ownership --- All TokenMaster Pools utilize a two-step ownership transfer process, implement access control for granting role-based permissions, and prevent ownership renouncing. #### Ownership Transfer Process --- 1. Current owner calls `transferOwnership(address newOwner)` specifying the address to transfer ownership to. * A `OwnershipTransferStarted(address,address)` event will be emitted when this function is called. * Until the new owner accepts ownership, the current owner will remain the owner and may transfer ownership to another account, canceling out the previous pending new owner. 2. New owner calls `acceptOwnership()` to become the contract owner. #### Roles --- In addition to the primary owner of a TokenMaster Token, tokens have role-based permissions that may be granted and revoked by the token owner. To grant a role the owner must call `grantRole(bytes32 role, address account)` to grant `role` (using role identifier) to `account`. To revoke a role the owner must call `revokeRole(bytes32 role, address account)` to revoke `role` (using role identifier) from `account`. There are two roles that are used in TokenMaster generally - `DEFAULT_ADMIN` and `ORDER_MANAGER`. An account with the default admin role will have permissions equivalent to the token owner in TokenMaster Router for setting token settings. An account with the order manager role will have permission to disable and re-enable specific advanced buy, sell and spend orders in TokenMaster Router. | Role | Role Identifier | | ------------- | -------------------------------------------------------------------- | | DEFAULT_ADMIN | `0x0000000000000000000000000000000000000000000000000000000000000000` | | ORDER_MANAGER | `0x3c6581d0b5cf40e66836017f19054f4ad048ab86f3953ec1b557d00fbaf735ac` | ### ERC-165 Interfaces --- TokenMaster Pools can be inspected with ERC-165's `supportsInterface(bytes4 interfaceId)` function to detect the pool's type and functionality it supports. This enables developers to programmatically query supported features of a pool, ensuring seamless integration. #### Overview ERC-165 is a standard for interface detection in Ethereum smart contracts. It defines a method to query whether a contract implements a specific interface. In the context of TokenMaster, it allows developers to identify pool types and their supported functionalities. #### Supported Interface IDs The following table lists the interface IDs and the corresponding pool types or functionalities they represent: | **Interface ID** | **Type / Functionality** | |-------------------|-------------------------------------------------------------------| | `0xb080171e` | General TokenMaster ERC20C interface (required by all pools). | | `0x1f4456b6` | Standard Pool functionality. | | `0x049ac6b3` | Creator emissions support. | | `0xcffb014c` | Promotional Pool functionality. | | `0xa18b736c` | Direct minting and burning support. | | `0x511e5ebf` | Stable Pool functionality. | #### Example Usage Below is an example of how developers can use the `supportsInterface` function to check for specific features in a pool: ```typescript // Address of the TokenMaster Pool contract const poolAddress: Address = '0xYourPoolAddressHere'; // Replace with the pool address // ERC-165 interface ID for Stable Pool functionality const stablePoolInterfaceId = '0x511e5ebf'; async function checkStablePoolSupport() { try { // Encode the supportsInterface call const callData = encodeFunctionData({ abi, functionName: 'supportsInterface', args: [stablePoolInterfaceId], }); // Perform the call const result = await client.readContract({ address: poolAddress, abi, functionName: 'supportsInterface', args: [stablePoolInterfaceId], }); if (result) { console.log('This pool supports Stable Pool functionality.'); } else { console.log('This pool does NOT support Stable Pool functionality.'); } } catch (error) { console.error('Error checking Stable Pool support:', error); } } ``` #### Ownership Renouncing --- TokenMaster Tokens prevent renouncing ownership due to the nature of the tokens creator controls and fees generated. Creators that may have been considering renouncing should consider all implications of not having full ownership access to a TokenMaster Token and alternatives such as governance contracts or timelocks that can hold ownership for a period of time. # Standard Pool Standard Pools offer flexibility and control with a wide range of levers for the creator to adjust, making them suitable for a wide range of tokenomics. The market value of tokens in a Standard Pool is derived from the ratio of paired tokens in the market to the total token supply. ## Standard Pool Settings --- - **Buy Fee** - Amount added to the market value cost of buying tokens. Creator may set both a percentage of market value buy fee and a flat rate per token. - **Sell Fee** - Amount subtracted from the market value of tokens being sold. Creator specified as a percentage. - **Buy Spread** - Amount as a percentage that tokens cost over the current market value. - **Sell Spread** - Amount as a percentage that tokens are worth under the current market value. - **Target Supply** - Scales buy cost when target supply is exceeded by the ratio of new supply to expected supply - charged as a "demand fee". - **Creator Share of Demand Fee** - Percentage of the demand fee that is allocated to the creator, the remainder goes to market value of tokens. - **Creator Share of Spends** - Percentage of the value of tokens spent that are allocated to the creator, the remainder goes to market value of tokens. - **Emissions** - Tokens the creator may mint at zero cost, up to a self-imposed limit that may only be lowered. Emissions are accrued over time and creator may reset their accrual / only claim a portion to delay the introduction of those zero cost tokens into the market. Since emissions are minted at zero cost they devalue the rest of the supply. ### Guardrails: To maintain fairness and predictability, these settings are constrained by immutable limits established during deployment: - **Fee Limits**: Caps on buy and sell fees. - **Spread Limits**: Minimum and maximum buy/sell spreads. - **Spend Share Limits**: Maximum percentage of spent tokens that can be allocated to the creator. ## Transactions --- **Buy:** A buyer will pay the market value of the tokens, adjusted by the buy spread, plus the creator buy fee plus a demand fee if the new supply exceeds the target supply (if applicable). Buy spread greater than zero will increase the market value of tokens. **Sell:** A seller will receive the market value of the tokens, adjusted by the sell spread, less the creator sell fee. Sell spread greater than zero will increase the market value of tokens. **Spend:** A spender will receive no market value for their tokens, market value will instead be transferred to the creator share at a rate specified by the creator. Creator share of spend less than 100% will increase the market value of tokens. **Claim Emissions:** Creator emissions are accrued at a rate specified during the deployment of the token and may be claimed at any time. Emissions are capped at an amount set at deployment, the cap may be lowered post-deployment but not increased. Claimed emissions by the creator will mint tokens at zero cost and decrease the market value of tokens. Creators may opt to forfeit a portion of a claim which will require the forfeitted emissions to be re-accrued before they can be claimed. ## Data Types --- ### StandardPoolInitializationParameters This struct defines parameters used for standard pool initialization. - **initialSupplyRecipient**: Address to receive the initial supply amount. - **initialSupplyAmount**: Initial amount of supply to mint to the initial supply recipient. - **minBuySpreadBPS**: Immutable minimum spread rate in BPS for buys. - **maxBuySpreadBPS**: Immutable maximum spread rate in BPS for buys. - **maxBuyFeeBPS**: Immutable maximum buy fee rate in BPS that may be set by the creator. - **maxBuyDemandFeeBPS**: Immutable maximum buy demand fee rate in BPS that may be set by the creator. - **maxSellFeeBPS**: Immutable maximum sell fee rate in BPS that may be set by the creator. - **maxSpendCreatorShareBPS**: Immutable maximum creator share rate in BPS that may be set by the creator. - **creatorEmissionRateNumerator**: Immutable numerator for the rate in tokens per second that a creator earns emissions. - **creatorEmissionRateDenominator**: Immutable denominator for the rate in tokens per second that a creator earns emissions. - **creatorEmissionsHardCap**: Initial value for the hard cap of total emissions that a creator may claim over time. - **initialBuyParameters**: Initial settings for buy parameters. - **initialSellParameters**: Initial settings for sell parameters. - **initialSpendParameters**: Initial settings for spend parameters. - **initialPausedState**: Initial paused state for pausable functions. ```solidity struct StandardPoolInitializationParameters { address initialSupplyRecipient; uint256 initialSupplyAmount; uint256 minBuySpreadBPS; uint256 maxBuySpreadBPS; uint256 maxBuyFeeBPS; uint256 maxBuyDemandFeeBPS; uint256 minSellSpreadBPS; uint256 maxSellSpreadBPS; uint256 maxSellFeeBPS; uint256 maxSpendCreatorShareBPS; uint256 creatorEmissionRateNumerator; uint256 creatorEmissionRateDenominator; uint256 creatorEmissionsHardCap; StandardPoolBuyParameters initialBuyParameters; StandardPoolSellParameters initialSellParameters; StandardPoolSpendParameters initialSpendParameters; uint256 initialPausedState; } ``` --- ### StandardPoolBuyParameters This struct defines parameters used for buys. - **buySpreadBPS**: Spread rate in BPS for the cost of tokens above the current market value. - **buyFeeBPS**: Fee rate in BPS for the fee applied to buys. - **buyCostPairedTokenNumerator**: The numerator for the ratio of paired token to pool token that will be added as a fee on top of buys. - **buyCostPoolTokenDenominator**: The denominator for the ratio of paired token to pool token that will be added as a fee on top of buys. - **useTargetSupply**: True if the pool uses a target supply for determining the application of demand fees. - **reserved**: Unused. Spacing for storage optimization. - **buyDemandFeeBPS**: Rate in BPS of the amount of demand fee that is allocated to the creator. - **targetSupplyBaseline**: Baseline supply amount for determining if a buy will exceed the target supply. - **targetSupplyBaselineScaleFactor**: Scale factor for the baseline supply. Stored as a separate value to optimize storage. - `Actual Supply Baseline = targetSupplyBaseline * 10^targetSupplyBaselineScaleFactor` - **targetSupplyGrowthRatePerSecond**: Rate in tokens per second that the target supply grows when calculating expected supply. - **targetSupplyBaselineTimestamp**: Unix timestamp for the time when target supply growth rate per second starts accumulating. - When the current block timestamp is less than `targetSupplyBaselineTimestamp`: - `Expected Supply = Actual Supply Baseline.` - When the current block timestamp is greater than `targetSupplyBaselineTimestamp`: - `Expected Supply = Actual Supply Baseline + targetSupplyGrowthRatePerSecond * (block.timestamp - targetSupplyBaselineTimestamp)` ```solidity struct StandardPoolBuyParameters { uint16 buySpreadBPS; uint16 buyFeeBPS; uint96 buyCostPairedTokenNumerator; uint96 buyCostPoolTokenDenominator; bool useTargetSupply; uint24 reserved; uint16 buyDemandFeeBPS; uint48 targetSupplyBaseline; uint8 targetSupplyBaselineScaleFactor; uint96 targetSupplyGrowthRatePerSecond; uint48 targetSupplyBaselineTimestamp; } ``` --- ### StandardPoolSellParameters This struct defines parameters used for sells. - **sellSpreadBPS**: Spread rate in BPS to adjust value of tokens being sold below market value. - **sellFeeBPS**: Sell fee rate in BPS that will be subtracted from a sell order. ```solidity struct StandardPoolSellParameters { uint16 sellSpreadBPS; uint16 sellFeeBPS; } ``` --- ### StandardPoolSpendParameters This struct defines parameters used for spends. - **creatorShareBPS**: Rate in BPS of the value of tokens that will be allocated to the creator when tokens are spent. - Any value not allocated to the creator remains in the market value of tokens and increases the market value of all remaining tokens. ```solidity struct StandardPoolSpendParameters { uint16 creatorShareBPS; } ``` --- ## Standard Pool Functions The following functions are specific to Standard Pools. Platforms integrating TokenMaster with Standard Pools should use these functions to update token settings, claim creator emissions, retrieve the current settings, and retrieve the guardrail settings for display purposes. ```solidity // Administrative functions. function setBuyParameters(StandardPoolBuyParameters calldata _buyParameters) external; function setSellParameters(StandardPoolSellParameters calldata _sellParameters) external; function setSpendParameters(StandardPoolSpendParameters calldata _spendParameters) external; function setEmissionsHardCap(uint256 newHardCapAmount) external; function claimEmissions(address claimTo, uint256 forfeitAmount) external; // Current settings, guardrails and target supply. function getBuyParameters() external view returns(StandardPoolBuyParameters memory); function getSellParameters() external view returns(StandardPoolSellParameters memory); function getSpendParameters() external view returns(StandardPoolSpendParameters memory); function targetSupply() external view returns(bool useTargetSupply, uint256 target); function getParameterGuardrails() external view returns( uint16 minBuySpreadBPS, uint16 maxBuySpreadBPS, uint16 maxBuyFeeBPS, uint16 maxBuyDemandFeeBPS, uint16 minSellSpreadBPS, uint16 maxSellSpreadBPS, uint16 maxSellFeeBPS, uint16 maxSpendCreatorShareBPS ); function getCreatorEmissions() external view returns( uint256 claimed, uint256 claimable, uint256 hardCap, uint48 lastClaim, uint128 creatorEmissionRateNumerator, uint128 creatorEmissionRateDenominator ); ``` ## Standard Pool Events Platforms integrating TokenMaster _should_ index the following events on all tokens that are relevant to the platform to know when a token's buy, sell or spend parameters need to be checked for updates, when a creator updates their emissions hard cap, or when a creator claims emissions. ```solidity /// @dev Emitted when the token's buy settings have been updated. event BuyParametersUpdated(); /// @dev Emitted when the token's sell settings have been updated. event SellParametersUpdated(); /// @dev Emitted when the token's spend settings have been updated. event SpendParametersUpdated(); /// @dev Emitted when the creator updates their hard cap for creator emissions. event CreatorEmissionsHardCapUpdated(uint256 newHardCapAmount); /// @dev Emitted when a creator claims emissions. event CreatorEmissionsClaimed(address to, uint256 claimedAmount, uint256 forfeitedAmount); ``` # Stable Pool Stable Pools provide fixed pricing for tokens, ideal for use cases where consistent value is essential, such as stablecoins or tightly controlled economies. Unlike Standard Pools, the market price in a Stable Pool remains constant, with fees applied to buy and sell transactions. ## Stable Pool Settings: Stable Pools include straightforward adjustable parameters: - **Buy Fee**: Additional cost applied to token purchases, defined as a percentage of the stable price. - **Sell Fee**: Reduction applied to token sales, defined as a percentage of the stable price. ## Guardrails: To maintain predictability and user trust, Stable Pools enforce immutable fee limits established during deployment: - **Fee Limits**: Caps on buy and sell fees to prevent excessive transaction costs. ## Transactions --- **Buy:** A buyer will pay the market value of the tokens plus the creator buy fee. **Sell:** A seller will receive the market value of the tokens less the creator sell fee. **Spend:** A spender will receive no market value for their tokens, market value will instead be transferred to the creator share. ## Data Types --- ### StablePoolInitializationParameters This struct defines parameters used for stable pool initialization. - **initialSupplyRecipient**: Address to receive the initial supply amount. - **initialSupplyAmount**: Initial amount of supply to mint to the initial supply recipient. - **maxBuyFeeBPS**: Immutable maximum buy fee rate in BPS that may be set by the creator. - **maxSellFeeBPS**: Immutable maximum sell fee rate in BPS that may be set by the creator. - **initialBuyParameters**: Initial settings for buy parameters. - **initialSellParameters**: Initial settings for sell parameters. - **stablePairedPricePerToken**: Immutable ratio of paired token to pool token for stable pricing. ```solidity struct StablePoolInitializationParameters { address initialSupplyRecipient; uint256 initialSupplyAmount; uint256 maxBuyFeeBPS; uint256 maxSellFeeBPS; StablePoolBuyParameters initialBuyParameters; StablePoolSellParameters initialSellParameters; PairedPricePerToken stablePairedPricePerToken; } ``` --- ### StablePoolBuyParameters This struct defines parameters used for buys. - **buyFeeBPS**: Buy fee rate in BPS that will be applied to a buy order. ```solidity struct StablePoolBuyParameters { uint16 buyFeeBPS; } ``` --- ### StablePoolSellParameters This struct defines parameters used for sells. - **sellFeeBPS**: Sell fee rate in BPS that will be subtracted from a sell order. ```solidity struct StablePoolSellParameters { uint16 sellFeeBPS; } ``` --- ### PairedPricePerToken This struct defines parameters used for spends. - **numerator**: The numerator for the ratio of paired token to pool token that will be the stable price of a token before fees. - **denominator**: The denominator for the ratio of paired token to pool token that will be the stable price of a token before fees. ```solidity struct PairedPricePerToken { uint96 numerator; uint96 denominator; } ``` --- ## Stable Pool Functions The following functions are specific to Stable Pools. Platforms integrating TokenMaster with Stable Pools should use these functions to update token settings, retrieve the current settings, and retrieve the guardrail settings for display purposes. ```solidity // Administrative functions. function setBuyParameters(StablePoolBuyParameters calldata _buyParameters) external; function setSellParameters(StablePoolSellParameters calldata _sellParameters) external; // Current settings and guardrails. function getBuyParameters() external view returns(StablePoolBuyParameters memory); function getSellParameters() external view returns(StablePoolSellParameters memory); function getStablePriceRatio() external view returns(uint96 numerator, uint96 denominator); function getParameterGuardrails() external view returns (uint16 maxBuyFeeBPS, uint16 maxSellFeeBPS); ``` ## Stable Pool Events Platforms integrating TokenMaster _should_ index the following events on all tokens that are relevant to the platform to know when a token's buy or sell parameters need to be checked for updates. ```solidity /// @dev Emitted when the token's buy settings have been updated. event BuyParametersUpdated(); /// @dev Emitted when the token's sell settings have been updated. event SellParametersUpdated(); ``` # Promotional Pool Promotional Pools are designed for tokens without inherent market value, typically used to incentivize user actions or reward participation. These tokens are still paired with another token to establish a buy rate for specific use cases. Creators may grant minting and burning roles to addresses to allow the minting of tokens without limitations and burning of tokens from any account without limitations or approvals. ## Promotional Pool Settings --- - **Buy Cost**: Specifies the flat rate of paired tokens required to purchase promotional tokens. Buy cost may be changed by the creator any time after deployment and has no guardrail to limit the cost. ## Transactions --- **Buy:** A buyer will pay the buy cost for the promotional tokens being purchased. **Sell:** Sales are disabled for this pool type as there is no market value. **Spend:** Share values are unchanged as there is no market value. ## Data Types --- ### PromotionalPoolInitializationParameters This struct defines parameters used for promotional pool initialization. - **initialSupplyRecipient**: Address to receive the initial supply amount. - **initialSupplyAmount**: Initial amount of supply to mint to the initial supply recipient. - **initialBuyParameters**: Initial settings for buy parameters. ```solidity struct PromotionalPoolInitializationParameters { address initialSupplyRecipient; uint256 initialSupplyAmount; PromotionalPoolBuyParameters initialBuyParameters; } ``` --- ### PromotionalPoolBuyParameters This struct defines parameters used for buys. - **buyCostPairedTokenNumerator**: The numerator for the ratio of paired token to pooled tokens when buying. - **buyCostPoolTokenDenominator**: The denominator for the ratio of paired token to pooled tokens when buying. ```solidity struct PromotionalPoolBuyParameters { uint96 buyCostPairedTokenNumerator; uint96 buyCostPoolTokenDenominator; } ``` --- ## Promotional Pool Functions The following functions are specific to Promotional Pools. Platforms integrating TokenMaster with Promotional Pools should use these functions to update token settings, retrieve the current settings for display purposes, or building infrastructure to support direct minting and burning of promo tokens. To call the `mint` and `mintBatch` functions, the caller must be granted the `MINTER_ROLE` for the Promotional Pool. To call the `burn` function, the caller must be granted the `BURNER_ROLE` for the Promotional Pool. | Role | Role Identifier | | ----------- | -------------------------------------------------------------------- | | MINTER_ROLE | `0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6` | | BURNER_ROLE | `0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848` | ```solidity // Administrative functions. function setBuyParameters(PromotionalPoolBuyParameters calldata _buyParameters) external; // Current settings and guardrails. function getBuyParameters() external view returns(PromotionalPoolBuyParameters memory); // Minting / Burning functions. function mint(address to, uint256 amount) external; function mintBatch(address[] calldata toAddresses, uint256[] calldata amounts) external; function burn(address from, uint256 amount) external; ``` ## Promotional Pool Events Platforms integrating TokenMaster *should* index the following events on all tokens that are relevant to the platform to know when a token's buy parameters need to be checked for updates. ```solidity /// @dev Emitted when the token's buy settings have been updated. event BuyParametersUpdated(); ``` # Contract Deployments TokenMaster can be permissionlessly deployed by any wallet to the same addresses on any EVM compatible chain. #### Contract Addresses: | Contract | Address | | ------------------------------------- | -------------------------------------------- | | **TokenMaster Router** | `0x0E00009d00d1000069ed00A908e00081F5006008` | | **Standard Pool Factory** | `0x000000c5F2DF717F497BeAcCE161F8b042310d17` | | **Stable Pool Factory** | `0x0000006a50a9c9Efae8875266ff222579fC2F449` | | **Promotional Pool Factory** | `0x00000014D04B7d1Cad1960eA8980A9af5De2104e` | ***Note: If TokenMaster is not yet deployed on your chain, use [the infrastructure deployment tool at developers.apptokens.com/infrastructure.](https://developers.apptokens.com/infrastructure)*** # Trusted Forwarder # Overview Trusted forwarders allow creators and marketplaces the ability to establish channels that become part of the order routing through Payment Processor. Channels allow for more granular trade attribution and metrics as well as the ability to block hostile apps from piggy-backing on the open Payment Processor protocol to vampire attack creators and marketplaces. There are two modes that a trusted forwarder can operate in - _open_ or _permissioned_. In _open_ mode the forwarder will relay all transactions with the caller's address appended to the transaction calldata. In _permissioned_ mode the forwarder will require the transaction data be signed by an address set by the forwarder's owner in order to be relayed. Creators may configure Payment Processor to only allow transactions that originate through specific channels (see [add trusted channel](../payment-processor/v3.0/for-creators/payment-settings#addtrustedchannelforcollection)) which, combined with a permissioned forwarder, allows creators to enforce their sales to be routed through specific frontend interfaces. ## How They Work Trusted forwarders work by receiving a function call from a user that contains the destination target and calldata to be relayed by the forwarder, appending the caller's address to the calldata and executing the call to the destination. Contracts that receive calls from the trusted forwarder check that the caller is a valid trusted forwarder and extract the original caller's address from the end of the calldata to use in place of the trusted forwarder's address as the caller of the function. For more information about deploying a trusted forwarder, continue to the [deployment](./deployment) page. # Deploying a Trusted Forwarder Trusted Forwarders ***MUST*** be deployed through the Limit Break Trusted Forwarder Factory to be considered a trusted forwarder for Payment Processor. To deploy a trusted forwarder call the `cloneTrustedForwarder` function on the Trusted Forwarder Factory. - `admin`: The initial owner of the trusted forwarder. The owner may update the forwarder's signer address, transfer and renounce ownership. - `appSigner`: For a *permissioned* forwarder this will be the signer address. For an *open* forwarder this must be set to `address(0)`. - `salt`: A 32-byte hex value that can only be used once per deployer address to create a deterministic deployment address for the trusted forwarder. *Note: Users that are mining a salt value for vanity/optimized addresses should be aware that the provided salt is hashed with their deployer address to determine the salt passed to the CREATE2 instruction.* ```solidity function cloneTrustedForwarder( address admin, address appSigner, bytes32 salt ) external returns (address trustedForwarder) ``` After a trusted forwarder is deployed, creators may set the trusted forwarder as an allowed channel with [add trusted channel](../payment-processor/v3.0/for-creators/payment-settings#addtrustedchannelforcollection) and block unauthorized channels by setting the `blockTradesFromUntrustedChannels` flag to true in [set collection payment settings](../payment-processor/v3.0/for-creators/payment-settings#setcollectionpaymentsettings). # Management Trusted forwarders are ownable smart contracts with the initial owner being the account address specified during their deployment. Ownership transfer and renounce follows standard Ownable contract methods. There are two functions on a trusted forwarder that are restricted to the contract owner - `updateSigner` and `deactivateSigner`. ## Update Signer `updateSigner` is used for one of two purposes: - To change the signer address from `address(0)` to a non-zero address to convert an *open* forwarder to a *permissioned* forwarder. - To update the signer address from a non-zero address to a different non-zero address if the dApp needs to sign with a different address for any reason. To make any of the above changes call `updateSigner` directly on the trusted forwarder contract from the account that is the forwarder owner with the appropriate `signer_` parameter value. ## Deactivate Signer `deactivateSigner` is used to change the signer address from a non-zero address to `address(0)` to convert a *permissioned* forwarder into an *open* forwarder. To deactivate the signer and convert the trusted forwarder into an *open* forwarder call `deactivateSigner` directly on the trusted forwarder contract from the account that is the forwarder owner. ## Functions ```solidity function updateSigner(address signer_) external; function deactivateSigner() external ``` # Usage Both *open* and *permissioned* trusted forwarders are designed to work with any smart contract that implements `TrustedForwarderERC2771Context` to create permissioned forwarding channels by routing user calls through the trusted forwarder. Trusted forwarder will *ALWAYS* append the caller of the trusted forwarder to the end of the calldata sent to the target address and forward the entire `msg.value` amount to the target. *Note: Trusted Forwarders do not have a fallback or receive function so they **CANNOT** receive payments from other contracts to refund excess payment amounts. Contracts that utilize a trusted forwarder should issue refunds directly to the appended caller address as native or wrapped tokens.* ## Open Trusted Forwarder Steps to execute a call through an *open* trusted forwarder: 1. Determine the target contract address for the call. 2. Determine the calldata for the transaction being sent to the target including the 4-byte function selector. 3. Call the trusted forwarder `forwardCall` with the `target`, `message`, and value to send. ```solidity function forwardCall( address target, bytes calldata message ) external payable returns (bytes memory returnData); ``` ## Permissioned Trusted Forwarder Steps to execute a call through an *open* trusted forwarder: 1. Determine the target contract address for the call. 2. Determine the calldata for the transaction being sent to the target including the 4-byte function selector. 3. Sign the message using the private key for the signer account that is set for the trusted forwarder. - The signature will be for an EIP712 structured message using: - The domain separator returned by the `domainSeparatorV4` view function - *with* - The message data being an AppSigner message defined [here](#appsigner-type-hash). 4. Call the trusted forwarder `forwardCall` with the `target`, `message`, `signature` and value to send. ```solidity function forwardCall( address target, bytes calldata message, SignatureECDSA calldata signature ) external payable returns (bytes memory returnData); ``` ### AppSigner Type Hash ``` AppSigner(bytes32 messageHash,address target,address sender) ``` # Contract Deployments #### Deployed Chains: - [Ethereum Mainnet](#ethereum) - [Base Mainnet](#base) - [Optimism Mainnet](#optimism) - [Arbitrum One Mainnet](#arbitrum) - [Polygon Mainnet](#polygon) - [Polygon zkEVM Mainnet](#polygon-zkevm) - [Avalanche C-Chain Mainnet](#avalanche-c-chain) - [Binance Smart Chain (BSC) Mainnet](#binance-smart-chain) - [Ethereum Sepolia Testnet](#ethereum-sepolia) #### Contract Addresses: | Contract | Address | |------------------------------------------------|----------------------------------------------| | Trusted Forwarder Implementation | `0xFF000047aBEA9064C699c0727148776e4E17771C` | | Trusted Forwarder Factory | `0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE` | ## Mainnet Deployments ### Ethereum - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://etherscan.io/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://etherscan.io/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Base - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://basescan.org/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://basescan.org/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Optimism - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://optimistic.etherscan.io/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://optimistic.etherscan.io/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Arbitrum - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://arbiscan.io/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://arbiscan.io/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Polygon - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://polygonscan.com/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://polygonscan.com/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Polygon zkEVM - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://zkevm.polygonscan.com/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://zkevm.polygonscan.com/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Binance Smart Chain - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://bscscan.com/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://bscscan.com/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ### Avalanche C-Chain - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://snowtrace.io/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://snowtrace.io/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) ## Testnet Deployments ### Ethereum Sepolia - Trusted Forwarder Implementation - [0xFF000047aBEA9064C699c0727148776e4E17771C](https://sepolia.etherscan.io/address/0xFF000047aBEA9064C699c0727148776e4E17771C) - Trusted Forwarder Factory - [0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE](https://sepolia.etherscan.io/address/0xFF0000B6c4352714cCe809000d0cd30A0E0c8DcE) # Wrapped Native # Overview **The Wrapped Native contract wraps native tokens (e.g. Ether) into an ERC-20 token. Wrapped Native is designed to be a more gas efficient, modern, more fully featured canonical replacement for WETH9 that can be deployed to a consistent, deterministic address on all chains.** Wrapped Native features the following improvements over WETH9. - **Deterministically Deployable By Anyone To A Consistent Address On Any Chain!** - **More Gas Efficient Operations Than WETH9!** - **`approve` and `transfer` functions are payable** - will auto-deposit when `msg.value > 0`. This feature will allow a user to wrap and approve a protocol in a single action instead of two, improving UX and saving gas. - **`depositTo`** - allows a depositor to specify the address to give WNATIVE to. Much more gas efficient for operations such as native refunds from protocols compared to `deposit + transfer`. - **`withdrawToAccount`** - allows a withdrawer to withdraw to a different address. - **`withdrawSplit`** - allows a withdrawer to withdraw and send native tokens to several addresses at once. - **Permit Functions** - allows for transfers and withdrawals to be approved to spenders/operators gaslessly using EIP-712 signatures. Permitted withdrawals allow gas sponsorship to unwrap wrapped native tokens on the user's behalf, for a small convenience fee specified by the app. This is useful when user has no native tokens on a new chain but they have received wrapped native tokens. ## Deployment Wrapped Native can be deployed to any EVM blockchain at address `0x6000030000842044000077551D00cfc6b4005900` using Limit Break's infrastructure deployment tool at https://developers.apptokens.com/infrastructure. # Usage Wrapped Native is backwards compatible with WETH9 - it includes all interfaces available in the original WETH. However, it includes additional features for better quality of life and better protocol integrations. This page covers typical usage scenarios. ## Deployment Wrapped Native can be deployed to any EVM blockchain at address `0x6000030000842044000077551D00cfc6b4005900` using Limit Break's infrastructure deployment tool at https://developers.apptokens.com/infrastructure. ## Depositing/Wrapping Native Tokens ### Native value transfer to contract A native token transfer to the contract without calldata will trigger a deposit via the `receive()` function. A native token transfer to the contract with calldata will trigger a deposit via the `fallback()` function. The `msg.sender` account will be credited with WNATIVE. ### deposit Function Calling the `deposit` function and including native value will credit the `msg.sender` account with WNATIVE. ```solidity /** * @notice Deposits `msg.value` funds into the `msg.sender` account, increasing their wrapped native token balance. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has increased by `msg.value`. * @dev 2. The `msg.sender`'s native token balance has decreased by `msg.value`. * @dev 3. The `msg.sender`'s wrapped native token balance has increased by `msg.value`. * @dev 4. A `Deposit` event has been emitted. The `msg.sender` address is logged in the event. */ function deposit() public payable; ``` ### depositTo Function Calling the `depositTo` function and including native value will credit the specified `to` account with WNATIVE, while depositing the `msg.sender` account's native funds. For protocols that issue native refunds by wrapping them and transferring the ERC-20 to the refundee, `depositTo` offers the most direct, gas-efficient mechanism. Some protocols may issue refunds this way to limit re-entrancy risk. ```solidity /** * @notice Deposits `msg.value` funds into specified user's account, increasing their wrapped native token balance. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has increased by `msg.value`. * @dev 2. The `msg.sender`'s native token balance has decreased by `msg.value`. * @dev 3. The `to` account's wrapped native token balance has increased by `msg.value`. * @dev 4. A `Deposit` event has been emitted. Caveat: The `to` address is logged in the event, not `msg.sender`. * * @param to The address that receives wrapped native tokens. */ function depositTo(address to) public payable; ``` ## Withdrawing/Unwrapping Native Tokens ### withdraw Function Calling the `withdraw` function will debit the `msg.sender` account's WNATIVE balance and credit them with the equivalent amount of native token. ```solidity /** * @notice Withdraws `amount` funds from the `msg.sender` account, decreasing their wrapped native token balance. * * @dev Throws when the `msg.sender`'s wrapped native token balance is less than `amount` to withdraw. * @dev Throws when the unwrapped native funds cannot be transferred to the `msg.sender` account. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has decreased by `amount`. * @dev 2. The `msg.sender`'s wrapped native token balance has decreased by `amount`. * @dev 3. The `msg.sender`'s native token balance has increased by `amount`. * @dev 4. A `Withdrawal` event has been emitted. The `msg.sender` address is logged in the event. * * @param amount The amount of wrapped native tokens to withdraw. */ function withdraw(uint256 amount) public; ``` ### withdrawToAccount Function Calling the `withdrawToAccount` function will debit the `msg.sender` account's WNATIVE balance, but credit the `to` account with the equivalent amount of native token. ```solidity /** * @notice Withdraws `amount` funds from the `msg.sender` account, decreasing their wrapped native token balance. * * @dev Throws when the `msg.sender`'s wrapped native token balance is less than `amount` to withdraw. * @dev Throws when the unwrapped native funds cannot be transferred to the `to` account. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has decreased by `amount`. * @dev 2. The `msg.sender`'s wrapped native token balance has decreased by `amount`. * @dev 3. The `to` account's native token balance has increased by `amount`. * @dev 4. A `Withdrawal` event has been emitted. Caveat: The `msg.sender` address is logged in the event, not `to`. * * @param to The address that receives the unwrapped native tokens. * @param amount The amount of wrapped native tokens to withdraw. */ function withdrawToAccount(address to, uint256 amount) public; ``` ### withdrawSplit Function Calling the `withdrawSplit` function will debit the `msg.sender` account's WNATIVE balance, but credit the specified amounts the multiple `to` accounts. ```solidity /** * @notice Withdraws funds from the `msg.sender` and splits the funds between multiple receiver addresses. * * @dev Throws when the `msg.sender`'s wrapped native token balance is less than the sum of `amounts` to withdraw. * @dev Throws when the unwrapped native funds cannot be transferred to one or more of the receiver addresses. * @dev Throws when the `toAddresses` and `amounts` arrays are not the same length. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has decreased by the sum of `amounts`. * @dev 2. The `msg.sender`'s wrapped native token balance has decreased by the sum of `amounts`. * @dev 3. The receiver addresses' native token balances have increased by the corresponding amounts in `amounts`. * @dev 4. A `Withdrawal` event has been emitted for each receiver address. Caveat: The `msg.sender` address is * logged in the events, not the receiver address. * * @param toAddresses The addresses that receive the unwrapped native tokens. * @param amounts The amounts of wrapped native tokens to withdraw for each receiver address. */ function withdrawSplit(address[] calldata toAddresses, uint256[] calldata amounts) external; ``` ## Approvals and Transfers ### approve Function ERC-20 WNATIVE approval for spenders, but with optional deposit feature. This is a powerful UX improvement, as protocols such as Seaport and Payment Processor no longer need separate deposit and approval calls when making offers. ```solidity /** * @notice Approves `spender` to spend/transfer `amount` of the `msg.sender`'s wrapped native tokens. * When `amount` is set to `type(uint256).max`, the approval is unlimited. * * @notice Unlike a typical ERC-20 token, this function is payable, allowing for a `deposit` and approval to be * executed simultaneously. If `msg.value` is greater than zero, the function will deposit the funds * into the `msg.sender` account before approving the `spender` to spend/transfer the funds. * If `msg.value` is zero, the function will only approve the `spender` to spend/transfer the funds. * This feature is intended to improve the UX of users using wrapped native tokens so that users don't have * to perform two transactions to first deposit, then approve the spending of their tokens, saving gas in * the process. * * @dev

Postconditions:

* @dev 1. The `spender` is approved to spend/transfer `amount` of the `msg.sender`'s wrapped native tokens. * @dev 2. A `Approval` event has been emitted. The `msg.sender` address, `spender` address, and `amount` of the * updated approval are logged in the event. * @dev 3. If `msg.value` is greater than zero, the `msg.sender`'s wrapped native token balance has increased by * `msg.value`. * @dev 4. If `msg.value` is greater than zero, a `Deposit` event has been emitted. The `msg.sender` address is * logged in the event. * * @param spender The address that is approved to spend/transfer the `msg.sender`'s wrapped native tokens. * @param amount The amount of wrapped native tokens that the `spender` is approved to spend/transfer. Approved * spending is unlimited when this values is set to `type(uint256).max`. * * @return Always returns `true`. */ function approve(address spender, uint256 amount) public payable returns (bool); ``` ### transfer Function ERC-20 WNATIVE transfer, but with optional deposit feature. ```solidity /** * @notice Transfers an `amount` of wrapped native tokens from the `msg.sender` to the `to` address. * * @notice If the `msg.value` is greater than zero, the function will deposit the funds into the `msg.sender` account * before transferring the wrapped funds. Otherwise, the function will only transfer the funds. * * @dev Throws when the `msg.sender` has an insufficient balance to transfer `amount` of wrapped native tokens. * * @dev

Postconditions:

* @dev 1. When `msg.value` is greater than zero, this contract's native token balance has increased by `msg.value`. * @dev 2. When `msg.value` is greater than zero, the `msg.sender`'s native token balance has decreased by `msg.value`. * @dev 3. When `msg.value` is greater than zero, the `msg.sender`'s wrapped native token balance has increased by `msg.value`. * @dev 4. When `msg.value` is greater than zero, a `Deposit` event has been emitted. The `msg.sender` address is logged in the event. * @dev 5. The `amount` of wrapped native tokens has been transferred from the `msg.sender` account to the `to` account. * @dev 6. A `Transfer` event has been emitted. The `msg.sender` address, `to` address, and `amount` are logged in the event. * * @param to The address that receives the wrapped native tokens. * @param amount The amount of wrapped native tokens to transfer. * * @return Always returns `true`. */ function transfer(address to, uint256 amount) public payable returns (bool); ``` ### transferFrom Function ERC-20 WNATIVE transferFrom, but with optional deposit feature. ```solidity /** * @notice Transfers an `amount` of wrapped native tokens from the `from` to the `to` address. * * @notice If the `msg.value` is greater than zero, the function will deposit the funds into the `from` account * before transferring the wrapped funds. Otherwise, the function will only transfer the funds. * @notice **As a reminder, the `msg.sender`'s native tokens will be deposited and the `from` (not the `msg.sender`) * address will be credited before the transfer. Integrating spender/operator protocols MUST be aware that * deposits made during transfers will not credit their own account.** * * @dev Throws when the `from` account has an insufficient balance to transfer `amount` of wrapped native tokens. * @dev Throws when the `msg.sender` is not the `from` address, and the `msg.sender` has not been approved * by `from` for an allowance greater than or equal to `amount`. * * @dev

Postconditions:

* @dev 1. When `msg.value` is greater than zero, this contract's native token balance has increased by `msg.value`. * @dev 2. When `msg.value` is greater than zero, the `msg.sender`'s native token balance has decreased by `msg.value`. * @dev 3. When `msg.value` is greater than zero, the `from` account's wrapped native token balance has increased by `msg.value`. * @dev 4. When `msg.value` is greater than zero, a `Deposit` event has been emitted. The `from` address is logged in the event. * @dev 5. The `amount` of wrapped native tokens has been transferred from the `from` account to the `to` account. * @dev 6. A `Transfer` event has been emitted. The `from` address, `to` address, and `amount` are logged in the event. * * @param from The address that transfers the wrapped native tokens. * @param to The address that receives the wrapped native tokens. * @param amount The amount of wrapped native tokens to transfer. * * @return Always returns `true`. */ function transferFrom(address from, address to, uint256 amount) public payable returns (bool); ``` ## Permits ***Note: Permit signatures may be signed by EOAs or smart contracts supporting EIP-1271.*** ### permitTransfer Function Allows a spender / protocol to transfer a user's WNATIVE tokens without a prior on-chain approval. Transfer permits are single-use, and can be used to pre-approve a spender to transfer a specific amount of WNATIVE token with a gasless signature. ```solidity /** * @notice Allows a spender/operator to transfer wrapped native tokens from the `from` account to the `to` account * using a gasless signature from the `from` account so that the `from` account does not need to pay gas * to set an on-chain allowance. * * @notice If the `msg.value` is greater than zero, the function will deposit the funds into the `from` account * before transferring the wrapped funds. Otherwise, the function will only transfer the funds. * @notice **As a reminder, the `msg.sender`'s native tokens will be deposited and the `from` (not the `msg.sender`) * address will be credited before the transfer. Integrating spender/operator protocols MUST be aware that * deposits made during transfers will not credit their own account.** * * @dev Throws when the `from` account is the zero address. * @dev Throws when the `msg.sender` does not match the operator/spender from the signed transfer permit. * @dev Throws when the permitAmount does not match the signed transfer permit. * @dev Throws when the nonce does not match the signed transfer permit. * @dev Throws when the expiration does not match the signed transfer permit. * @dev Throws when the permit has expired. * @dev Throws when the requested transfer amount exceeds the maximum permitted transfer amount. * @dev Throws when the permit nonce has already been used or revoked/cancelled. * @dev Throws when the master nonce has been revoked/cancelled since the permit was signed. * @dev Throws when the permit signature is invalid, or was not signed by the `from` account. * @dev Throws when the `from` account has an insufficient balance to transfer `transferAmount` of wrapped native tokens. * * @dev

Postconditions:

* @dev 1. When `msg.value` is greater than zero, this contract's native token balance has increased by `msg.value`. * @dev 2. When `msg.value` is greater than zero, the `msg.sender`'s native token balance has decreased by `msg.value`. * @dev 3. When `msg.value` is greater than zero, the `from` account's wrapped native token balance has increased by `msg.value`. * @dev 4. When `msg.value` is greater than zero, a `Deposit` event has been emitted. The `from` address is logged in the event. * @dev 5. `nonce` for `from` account is invalidated. * @dev 6. A `PermitNonceInvalidated` event has been emitted. * @dev 7. The `transferAmount` of wrapped native tokens has been transferred from the `from` account to the `to` account. * @dev 8. A `Transfer` event has been emitted. The `from` address, `to` address, and `transferAmount` are logged in the event. * * @param from The address that transfers the wrapped native tokens. * @param to The address that receives the wrapped native tokens. * @param transferAmount The amount of wrapped native tokens to transfer. * @param permitAmount The maximum amount of wrapped native tokens that can be transferred, signed in permit. * @param nonce The nonce, signed in permit. * @param expiration The expiration timestamp, signed in permit. * @param signedPermit The signature of the permit. */ function permitTransfer( address from, address to, uint256 transferAmount, uint256 permitAmount, uint256 nonce, uint256 expiration, bytes calldata signedPermit ) external payable ``` The permit is a signed EIP-712 message that matches the following format/typehash and domain separator. Domain separator values are: - name: "Wrapped Native" - version: "1" - chainId: ***Specify the chainId of the chain where the permit is valid.*** - verifyingContract: `0x6000030000842044000077551D00cfc6b4005900` ```solidity bytes32 private constant DOMAIN_SEPARATOR = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); bytes32 constant PERMIT_TRANSFER_TYPEHASH = keccak256("PermitTransfer(address operator,uint256 amount,uint256 nonce,uint256 expiration,uint256 masterNonce)"); ``` ### doPermittedWithdraw Function Allows a user to withdraw or unwrap WNATIVE tokens on a user's behalf. This is useful when a user has WNATIVE tokens, but has insufficient native funds to withdraw or unwrap themselves. In this case, the permitted operator could be an app that charges a small convenience fee for the withdrawal service. Note that convenience fees are taxed by the protocol at a rate of 10%. So if the convience fee is 0.25%, the protocol fee will be 0.025% and the convenience fee receiver gets to keep 0.225% of the withdrawn amount. Convenience fees for the app are acknowledged in the user's signed permit. ```solidity /** * @notice Allows a spender/operator to withdraw wrapped native tokens from the `from` account to the `to` account * using a gasless signature signed by the `from` account to prove authorization of the withdrawal. * * @dev Throws when the `from` account is the zero address. * @dev Throws when the `msg.sender` does not match the operator/spender from the signed withdrawal permit. * @dev Throws when the permit has expired. * @dev Throws when the amount does not match the signed withdrawal permit. * @dev Throws when the nonce does not match the signed withdrawal permit. * @dev Throws when the expiration does not match the signed withdrawal permit. * @dev Throws when the convenience fee reciever and fee does not match the signed withdrawal permit. * @dev Throws when the `to` address does not match the signed withdrawal permit. * @dev Throws when the permit nonce has already been used or revoked/cancelled. * @dev Throws when the master nonce has been revoked/cancelled since the permit was signed. * @dev Throws when the permit signature is invalid, or was not signed by the `from` account. * @dev Throws when the `from` account has an insufficient balance to transfer `transferAmount` of wrapped native tokens. * * @dev

Postconditions:

* @dev 1. This contract's native token balance has decreased by `amount`, less convenience and infrastructure * fees that remain wrapped. * @dev 2. The `from` account's wrapped native token balance has decreased by `amount`. * @dev 3. The `to` account's native token balance has increased by `amount`, less convenience and/or infrastructure fees. * @dev 4. The `convenienceFeeReceiver` account's wrapped native token balance has increased by the convenience fee. * @dev 5. The infrastructure tax account's wrapped native token balance has increased by the infrastructure fee. * @dev 6. `nonce` for `from` account is invalidated. * @dev 7. A `PermitNonceInvalidated` event has been emitted. * @dev 8. A `Withdrawal` event has been emitted. Caveat: The `from` address is logged in the event, not `to` or `msg.sender`. * * @param from The address that from which funds are withdrawn. * @param to The address that receives the withdrawn funds. * @param amount The amount of wrapped native tokens to withdraw. * @param nonce The nonce, signed in permit. * @param expiration The expiration timestamp, signed in permit. * @param convenienceFeeReceiver The address that receives the convenience fee. * @param convenienceFeeBps The basis points of the convenience fee. * @param signedPermit The signature of the permit. */ function doPermittedWithdraw( address from, address to, uint256 amount, uint256 nonce, uint256 expiration, address convenienceFeeReceiver, uint256 convenienceFeeBps, bytes calldata signedPermit ) external; ``` The withdrawal permit is a signed EIP-712 message that matches the following format/typehash and domain separator. Domain separator values are: - name: "Wrapped Native" - version: "1" - chainId: ***Specify the chainId of the chain where the permit is valid.*** - verifyingContract: `0x6000030000842044000077551D00cfc6b4005900` ```solidity bytes32 private constant DOMAIN_SEPARATOR = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); bytes32 constant PERMIT_WITHDRAWAL_TYPEHASH = keccak256("PermitWithdrawal(address operator,uint256 amount,uint256 nonce,uint256 expiration,uint256 masterNonce,address to,address convenienceFeeReceiver,uint256 convenienceFeeBps)"); ``` ## Cancelling Permits ### revokeMyOutstandingPermits Function To cancel ALL outstanding permits (both transfer and withdrawal), use the `revokeMyOutstandingPermits` function, which will increment the user's master nonce. The user must call this function themselves. ```solidity /** * @notice Allows the `msg.sender` to revoke/cancel all prior permitted transfer and withdrawal signatures. * * @dev

Postconditions:

* @dev 1. The `msg.sender`'s master nonce has been incremented by `1` in contract storage, rendering all signed * permits using the prior master nonce unusable. * @dev 2. A `MasterNonceInvalidated` event has been emitted. */ function revokeMyOutstandingPermits() external; ``` ### revokeMyNonce Function To cancel a single transfer or withdrawal permit, use the `revokeMyNonce` function, which will permanently invalidate the supplied permit nonce. ```solidity /** * @notice Allows the `msg.sender` to revoke/cancel a single, previously signed permitted transfer or withdrawal * signature by specifying the nonce of the individual permit. * * @dev Throws when the `msg.sender` has already revoked the permit nonce. * @dev Throws when the permit nonce was already used successfully. * * @dev

Postconditions:

* @dev 1. The specified `nonce` for the `msg.sender` has been revoked and can * no longer be used to execute a permitted transfer or withdrawal. * @dev 2. A `PermitNonceInvalidated` event has been emitted. * * @param nonce The nonce that was signed in the permitted transfer or withdrawal. */ function revokeMyNonce(uint256 nonce) external; ``` # Gas Benchmarks ## Raw Savings Per Function | Function | Gas Saved WNATIVE vs WETH9 | |-----------------------------------|----------------------------| | balanceOf | 88 | | native transfer (deposit) | 48 | | deposit | 108 | | withdraw | 395 | | approve | 213 | | transfer | 627 | | transferFrom (Self Transfer) | 481 | | transferFrom (Operator Allowance) | 1080 | | transferFrom (Operator Unlimited) | 508 | ## Aggregate L1 Savings ($2189.25 Avg USD/ETH) | Operation | Savings Per Operation | ETH Mainnet Operations (2022/09/15 - 2024/10/28) | Gas Units Saved | Avg Post-Merge Gas Price | ETH Saved | |---------------|-------------------------|--------------------------------------------------|-----------------|--------------------------|---------------| | balanceOf | 88 | 327,453,459 | 29B | 21.6 gwei | 626E | | transfer | 627 | 179,154,025 | 112B | 21.6 gwei | 2419E | | withdraw | 395 | 61,370,490 | 24B | 21.6 gwei | 518E | | transferFrom | 794 | 24,489,452 | 19.5B | 21.6 gwei | 421E | | approve | 213 | 6,936,611 | 1.5B | 21.6 gwei | 32E | | deposit | 108 | 125,633 | 13.5M | 21.6 gwei | 0E | | total | | | 186B | 21.6 gwei | 4016E ($8.8M) | ***Fun Fact: The eight zero-bytes mined in the official Wrapped Native address would save an additional 96 gas in calldata. Accounting for additional calldata gas savings, it is estimated that and additional $1.2M would have been saved!*** ***Note: Benchmarks focused only on interfaces that are shared between WETH9 and WNATIVE, and do not include measurements for features that did not exist in WETH9. Benchmarks were prepared and executed so that the identical conditions were recreated on both contracts in order to achieve and accurate comparison between contracts.*** # AMM Cookbook The AMM Cookbook is a developer resource for designing programmable liquidity, trading logic, and market access using the **Limit Break AMM (LBAMM)**. Built from the ground up for customization, **Limit Break AMM** supports hooks at the token, pool, and position level, giving developers full control over how assets are traded, how fees are collected, and who can access liquidity. Apptokens are at the heart of this system. By enforcing transfer rules and permitting token movement through approved operators, Apptokens ensure tokens can only be traded through authorized protocols like LBAMM. This gives developers confidence that their fee, access, and reward logic can’t be bypassed by routing trades through other AMMs or OTC. This document outlines a wide range of programmable AMM patterns, from dynamic fees and swap gating to custom market-making strategies. Each entry introduces a novel design, the use case it serves, and how it can be implemented using Limit Break’s programmable infrastructure. This list is non-exhaustive. As developers continue to build new primitives on top of the protocol, the design space will expand and unlock entirely new categories of trading and liquidity behavior. ## Categories 1. [Fee Design Patterns](#fee-design-patterns) 2. [Swap Access & Reward Mechanics](#swap-access--reward-mechanics) 3. [Liquidity Access & Incentives](#liquidity-access--incentives) 4. [Market Making & Price Control](#market-making--price-control) --- ## Fee Design Patterns Fee Design Patterns describe how fees can be customized, routed, and segmented across tokens, pools, and users. These patterns enable developers to design tokenomics that reward long-term users, penalize bots, generate protocol revenue, or fund ecosystems in novel ways. ### What new opportunities do fee design patterns unlock? - [Ensure fee integrity through approved execution paths](#ensure-fee-integrity-through-approved-execution-paths) - [Reduce trading fees the longer a token is held](#reduce-trading-fees-the-longer-a-token-is-held) - [Take fees in the paired token](#take-fees-in-the-paired-token) - [Take fees in the native token](#take-fees-in-the-native-token) - [Token-based fee segmentation by user attributes](#token-based-fee-segmentation-by-user-attributes) - [Pool-based fee segmentation by user traits](#pool-based-fee-segmentation-by-user-traits) - [Scale pool fees based on transaction size](#scale-pool-fees-based-on-transaction-size) - [Adjust fees based on historical trading volume](#adjust-fees-based-on-historical-trading-volume) - [Adjust fees based on profit and loss](#adjust-fees-based-on-profit-and-loss) - [Enforce marketplace fees on offchain orders](#enforce-marketplace-fees-on-offchain-orders) - [Monetize routing of offchain orders through liquidity pools](#monetize-routing-of-offchain-orders-through-liquidity-pools) - [Charge fees in native token when used in a flash loan](#charge-fees-in-native-token-when-used-in-a-flash-loan) - [Charge fees in alternate token when used in a flash loan](#charge-fees-in-alternate-token-when-used-in-a-flash-loan) --- ## Ensure fee integrity through approved execution paths ### How can tokens ensure fees are consistently applied across venues? Apptokens can authorize specific operator contracts, such as LBAMM, so trades are executed through paths where hooks apply fee, reward, and policy logic. This guarantees consistent, predictable economics while preserving composability. For example: # - A token `$ROYALTY` routes 1% of swap value to the protocol treasury. - As an Apptoken, it authorizes transfers through LBAMM pools (and other approved operators) where fee logic is applied automatically. - All valid transfers execute via these sanctioned paths, ensuring the treasury receives its share and token rules are honored. --- ## Reduce trading fees the longer a token is held ### How can tokens reduce trading fees over time? Tokens can be implemented with dynamic fee logic that rewards long-term holders by decreasing trading fees based on how long the token has been held. This unlocks a powerful alignment between holders and the long-term goals of a protocol or product, as users are financially incentivized to delay selling. Developers can configure fee decay curves to match product retention goals or market stabilization needs. For example: # - A protocol launches a token, `$HOLD`, that accrues utility value in the ecosystem over time. - The token is implemented with time-sensitive fee logic - 5% fee if held < 24 hours, 2% fee if held < 7 days, 0.5% if held longer. - Speculators who immediately trade the token incur a high fee, while loyal users enjoy near-free trades. - Trading volume stabilizes over time and aligns with long-term user growth. --- ## Take fees in the paired token ### How can token trading fees be collected in the paired token? Instead of taking trading fees in the native token being transferred, tokens can be configured to collect fees in the paired token used for the trade. This is especially useful when the developer prefers to accumulate ETH, USDC, or another stable asset. For example: # - A game developer launches a token, `$XP`, that’s earned through gameplay and spent on upgrades. - When players buy `$XP` in a LBAMM pool paired with ETH, the developer takes a 2% fee in ETH. - Players receive the full amount of `$XP` they paid for, and the game treasury accumulates ETH for development and prize pools. --- ## Take fees in the native token ### How can tokens charge fees in their own supply? Tokens can be configured to always take fees in their own native supply, even when being paired against other tokens. This design is useful when token deflation, burn mechanics, or internal redistribution are key to the tokenomics of the system. This creates tight feedback loops where usage drives scarcity or redistribution without relying on paired token behavior. For example: # - A DeFi protocol launches `$BURN`, a governance token with a deflationary design. - When users sell `$BURN` for USDC, a 1% fee is taken in `$BURN`, not in USDC. - The fee is burned permanently, reducing supply with each swap. - As activity increases, the supply of `$BURN` decreases, creating long-term demand pressure. --- ## Token-based fee segmentation by user attributes ### How can a token apply different fees based on who is trading it? Tokens can implement their own fee logic via hooks that assess attributes of the trader’s wallet. These attributes might include governance participation, reputation scores, or social graph data. The fee is enforced by the token itself, regardless of which pool it's being traded in. This gives token creators a protocol-level tool to reward long-term holders, penalize bots, or create community-based fee tiers across all liquidity venues. For example: # - A token `$ENGAGE` is launched for a decentralized social app. - The token defines a fee hook that charges different fees based on wallet behavior: - Contributors with over 50 DAO votes: 0.1% fee - New wallets: 0.4% fee - Known bot wallets (Sybil-flagged): 1.0% fee - These fees apply no matter what pool the token is traded in. - The result: power users pay less, low-quality traffic pays more, all enforced at the token layer. --- ## Pool-based fee segmentation by user traits ### How can individual pools charge different fees to different users? Pools can define custom fee logic that charges different swap fees depending on who the trader is, even if the token itself has no built-in fee logic. These hooks are set at the pool level, giving LPs or protocols granular control over how their specific liquidity is priced. This enables powerful use cases like staking discounts, affiliate pricing, or loyalty-based pool access. For example: # - A LBAMM pool is created for `$TOKEN` / ETH. - The pool implements a fee hook that checks if the trader is staking `$TOKEN` in a rewards contract. - Active stakers: 0.2% fee - Non-stakers: 0.6% fee - Another pool might offer discounts to wallets holding a specific NFT. - Each pool sets its own logic, giving LPs control over pricing and user targeting. --- ## Scale pool fees based on transaction size ### How can pools apply different fees to large and small trades? Fees can be scaled based on the size of the transaction, using predefined brackets or continuous curves. This discourages toxic flow or MEV while making small trades more affordable. Developers can also implement premium pricing for high-volume access or subsidized micro-transactions to promote inclusivity. For example: # - A game studio launches `$GEM` with a pool designed for in-game swaps. - Fees scale as follows: - Trades under $10: 0.1% - Trades between $10 and $1,000: 0.4% - Trades over $1,000: 1% - This encourages lightweight usage inside the game while preventing large value extraction from external players or bots. --- ## Adjust fees based on historical trading volume ### How can pools personalize fees based on past trading behavior? Pools and tokens can adjust fee rates based on a user’s historical trading volume. This allows for loyalty programs, whale-tier pricing, or anti-sandwich protections based on a wallet’s long-term usage profile. Hooks can read trade histories, on-chain volumes, or protocol-specific metrics to apply contextual fees dynamically. For example: # - A DEX rewards long-term traders by lowering fees for wallets with high historical usage. - Wallets with over $1M in past volume pay just 0.2% per trade. - New wallets pay 0.6% until they’ve traded enough to qualify for volume tiers. - Fee logic is implemented in a pool-level hook that reads a usage-tracking contract and applies tiered pricing. --- ## Adjust fees based on profit and loss ### How can fees reward users in a loss and tax those in profit? Swap fees can be dynamically adjusted based on a trader’s net profit or loss in a given token or ecosystem. This allows developers to subsidize users who are trading at a loss, offering them cheaper exit liquidity, while extracting more value from users who are actively profiting. It’s a powerful tool for smoothing volatility, reducing sell pressure from distressed holders, and capturing upside from high-frequency or MEV-driven traders. For example: # - A game token, `$GRIT`, tracks each wallet’s average entry price. - When a player swaps `$GRIT`, the pool hook checks if they’re currently above or below breakeven. - If they’re trading below their average entry price (a net loss), they pay only 0.2% in fees. - If they’re exiting at a profit, they pay a higher 1.0% fee that funds rewards or treasury growth. - This cushions losses for loyal players and charges more from opportunistic sellers. --- ## Enforce marketplace fees on offchain orders ### How can marketplaces ensure signed orders include non-removable fees? When using signed offchain orders, protocols can enforce marketplace fees at the time of swap execution by validating the fee terms included in the signed payload. This ensures the fee set by the order maker cannot be bypassed by takers or routing agents. This pattern allows marketplaces and RFQ systems to reliably monetize order flow while still offering decentralized execution. For example: # - A signed order to sell 1,000 `$TOKEN` for ETH includes a 0.25% marketplace fee. - The order maker signs this fee into the offchain order structure. - When the order is filled through a LBAMM pool, a hook verifies that the expected fee was included and properly routed. - Takers cannot modify or remove the fee, and the marketplace hosting the orderbook earns revenue for providing liquidity access. --- ## Monetize routing of offchain orders through liquidity pools ### How can searchers earn from routing signed orders into pools? When signed orders are filled using public liquidity pools, searchers (a.k.a. solvers or routers) can attach an execution fee that captures a portion of the swap. This “fee on top” can be dynamically calculated and routed to the entity that fulfilled the order. This model rewards routing and execution infrastructure while preserving the user’s intent and slippage bounds. For example: # - A user signs a permit to sell 1,000 `$TOKEN` for at least 0.5 ETH. - A searcher finds a LBAMM route that can fulfill the order for 0.55 ETH. - The searcher adds a 0.05 ETH fee-on-top and executes the swap on-chain. - The user receives 0.50 ETH, and the searcher earns 0.05 ETH for execution. --- ## Charge fees in native token when used in a flash loan ### How can tokens apply fees only during flash loan usage? Tokens can be configured to remain fee-free during normal transfers or trades, but apply a fee when used in a flash loan. This behavior allows developers to extract value specifically from MEV searchers or arbitrage bots that borrow and repay tokens in a single transaction. The logic detects flash usage through execution context or balance deltas, and applies a token-level fee conditionally. For example: # - A stablecoin project launches `$USDX` with no standard trading fee. - When `$USDX` is borrowed and returned within the same transaction (flash loan behavior), a 0.3% fee in `$USDX` is enforced. - This fee is routed to a protocol treasury. - Honest users can transfer or trade normally, while advanced use cases like arbitrage contribute to the ecosystem. --- ## Charge fees in alternate token when used in a flash loan ### How can tokens collect flash loan fees in a different token? Instead of charging flash loan fees in the borrowed token itself, developers can implement logic to charge fees in an alternate asset such as ETH or a governance token. This gives more flexibility in treasury strategy and decouples the utility of the token from its fee accrual. Hook logic can read execution context and enforce multi-token fee transfers atomically. For example: # - A yield protocol allows `$YIELD` to be flash borrowed for composability. - When borrowed in a flash loan, the protocol charges a 0.5% fee but in `$GOV`, its governance token. - LBAMM verifies both the return of the original amount and the presence of the fee in `$GOV` during the same transaction. - This model boosts demand for `$GOV` while allowing flexible use of `$YIELD` in complex strategies. --- ## Swap Access & Reward Mechanics Swap Access & Reward Mechanics let developers define who can trade, under what conditions, and with what incentives. These patterns empower games, compliance-focused apps, and programmable markets to tailor participation while embedding custom rewards into every trade. ### What new opportunities do swap access & reward mechanics unlock? - [Allow swap access based on NFT ownership](#allow-swap-access-based-on-nft-ownership) - [Allow trading for KYC-verified users](#allow-trading-for-kyc-verified-users) - [Enable swaps through game level progression](#gate-swaps-by-game-level-progression) - [Enable swaps based on net worth](#enable-swaps-based-on-net-worth) - [Allow swaps within specific price ranges](#allow-swaps-within-specific-price-ranges) - [Meter order flow through exchange integrations](#meter-order-flow-through-exchange-integrations) - [Reward exchanges for routing order flow](#reward-exchanges-for-routing-order-flow) - [Reward buyers with spend-only tokens](#reward-buyers-with-spend-only-tokens) --- ## Allow swap access based on NFT ownership Hooks can be used to allow swap access based on whether the wallet holds a specific NFT or belongs to an NFT collection. This is ideal for games, clubs, gated airdrops, or communities where only members should be able to access liquidity. NFT gating can be applied at the token level, pool level, or even per liquidity position. For example: # - A music artist launches a fan token, `$VIBES`, tradable only by holders of a concert ticket NFT. - When a user attempts a swap, the token-level hook checks for NFT ownership. - If the user holds the required NFT, the trade is allowed. - NFT holders can swap freely, creating exclusive access to rewards or merch tokens. --- ## Allow trading for KYC-verified users Hooks can be used to allow trading only for wallets that have passed KYC verification. This is ideal for regulated markets, institutional token offerings, or any environment where compliance is required. KYC checks can be applied at the token level, pool level, or per liquidity position. For example: # - A security token, `$RWA`, is tradable only by investors who have completed identity verification. - When a user attempts a swap, the token-level hook verifies KYC status. - If the user is KYC-verified, the trade is allowed. - Verified users can trade freely within the pool, ensuring compliance without hindering legitimate access. --- ## Enable swaps through game level progression ### How can in-game achievements unlock token access? Game developers can allow swaps for wallets that have achieved specific progress in a game such as reaching a level, owning a character, or completing a quest. This turns token access into an earned privilege and unlocks novel progression-based economies. The hook logic reads game state contracts to validate eligibility during the swap. For example: # - A game launches `$LOOT`, a reward token usable for high-tier upgrades. - Players who have reached level 50 can swap into `$LOOT`. - The pool hook checks the caller’s level via the game’s on-chain player stats contract. - Progression is recognized at the liquidity layer, turning gameplay into token access. --- ## Enable swaps based on net worth ### How can swap access be tailored to a trader's portfolio value? Protocols can design pools that cater to traders within specific net worth ranges. This creates differentiated market access, offering dedicated liquidity for institutional traders, or reserving early distribution for everyday users. Net worth can be estimated using token balances, staked positions, NFTs, or external oracles. For example: # - A compliance-focused `$RWA` pool enables swaps for wallets with over $250,000 in on-chain assets, verified using a net worth oracle. Ensuring access for institutional-grade traders in regulated markets. - A fair launch `$COMMUNITY` pool enables swaps for wallets with under $5,000 in assets, ensuring broader distribution among everyday participants while preventing early whale dominance. --- ## Allow swaps within specific price ranges ### How can tokens or pools enforce pricing constraints? Using Limit Break AMM’s hook system, swaps can be enabled only when the current price is within a defined range. This supports use cases like launch mechanics, price bands, or range-bound stability tokens. Developers can define static price ceilings/floors or dynamic ranges based on oracles or market state. For example: # - A project launches `$SALE`, a fixed-price token distribution. - The token is configured to allow swaps only while the price is between 1.00 and 1.50 USDC. - If the price moves outside the band, swaps pause until the price re-enters the range. - This ensures the token trades within the intended price window during initial distribution. --- ## Meter order flow through exchange integrations ### How can exchanges be charged for routing trades through your pools? Protocols can meter swap volume based on the source of order flow, such as a specific DEX frontend, aggregator, or backend service, and enforce limits, apply fees, or route incentives accordingly. This gives pool developers the ability to monetize high-volume integrations and ensure that exchanges pay their fair share. Hooks can inspect transaction metadata or enforce access tokens to apply usage-based rules. For example: # - A DEX aggregator integrates LBAMM and routes thousands of swaps per hour through a high-liquidity pool. - The pool developer configures their hook to meters trades by origin and enforces a usage cap of 50,000 swaps per day from that aggregator. - Once the cap is hit, the aggregator must pay an additional routing fee or request expanded access. - Smaller exchanges are allowed higher burst limits or subsidized fees to encourage growth. --- ## Reward exchanges for routing order flow ### How can liquidity pools incentivize integrations and routing? Protocols can offer dynamic rebates to exchanges or dApps that route users to their liquidity pools. These rebates can be paid in the swapped token, the paired token, or a third-party incentive token. Routing metadata is passed through calldata and validated via hooks, enabling affiliate-style reward structures. For example: # - A LBAMM pool offers a 0.05% rebate to any exchange that routes volume through it. - When a user swaps via PartnerDEX, the frontend sets the PartnerDEX address as the exchange fee recipient. - The pool-level hook verifies the recipient and sends the rebate to PartnerDEX’s wallet. - Exchanges are now incentivized to direct users to deeper or more strategic liquidity. --- ## Reward buyers with spend-only tokens ### How can swaps grant tokens that can’t be resold? Tokens can reward buyers with non-transferable, spend-only tokens when they trade through a specific pool. These tokens can be used for in-game items, upgrades, or other utilities but can’t be resold or dumped. This mechanic aligns rewards with usage and unlocks more sustainable incentive structures. For example: # - A game studio launches `$BATTLE` for in-game PVP and pairs it against ETH. - When players buy `$BATTLE`, they also receive a matching amount of `$MEDAL`, a spend-only token. - `$MEDAL` can be redeemed for skins or cosmetics but cannot be transferred or sold. - Loyalty rewards are earned through actual purchases, not airdrop farming. --- ## Liquidity Access & Incentives Liquidity Access & Incentive Design lets developers shape who can provide liquidity, what pairings are supported, and how LP rewards are distributed. These patterns enable intentional market design, maximize capital efficiency, and create targeted participation incentives. ### What new opportunities do liquidity access & incentives unlock? - [Allow specific token pairings](#allow-specific-token-pairings) - [Allow approved creators for liquidity pools](#allow-approved-creators-for-liquidity-pools) - [Enable LP fee sharing with builders](#enable-lp-fee-sharing-with-builders) - [Enable liquidity provision for verified users](#enable-liquidity-provision-for-verified-users) - [Enable liquidity provision for specific character classes or levels](#enable-liquidity-provision-for-specific-character-classes-or-levels) --- ## Allow specific token pairings ### How can developers control what tokens can be paired? Token hooks can implement rules that approve only designated token pairings. This ensures liquidity is concentrated in trusted markets, prevents fragmentation, and maintains security against malicious pairings. Pair allowances can be enforced at pool creation time using token-level hooks. For example: # - A stablecoin project launches `$USDX` and limits it to approved pairs. - Only pools pairing `$USDX` with ETH, USDC, or `$YIELD` are allowed. - The token hook enforces this rule, ensuring all pairs meet the approved list. - Liquidity stays focused in high-quality, vetted markets. --- ## Allow approved creators for liquidity pools ### How can pool creation be limited to approved users? Developers can enable pool creation only for specific roles, contracts, or KYC/AML-verified addresses. This helps maintain curated, high-quality liquidity environments, support launch phases, or safeguard protocol integrity. Token-level hooks can check wallet attributes before allowing a pool to be initialized. For example: # - A lending protocol launches a permissioned pool system for interest-bearing tokens. - Verified partners and DAO-curated addresses can deploy new pools. - When a wallet calls the pool creation function, the hook checks an allowlist. - Authorized creators ensure every pool meets quality and compliance standards. --- ## Enable LP fee sharing with builders ### How can hook developers or protocols earn from LP activity? LBAMM allows liquidity pool hooks to direct a portion of swap fees to third parties including protocol treasuries, staking systems, or independent hook developers. This creates sustainable incentives for building valuable liquidity logic. Instead of charging users more, the fee share comes from the LP's portion rewarding infrastructure without increasing execution prices. For example: # - A developer publishes a public hook that implements advanced anti-bot logic and dynamic fee scaling. - LPs opt to use the hook to improve pool performance and attract better order flow. - The hook takes a 0.5% swap fee, split as: - 0.4% to LPs - 0.1% to the hook developer’s wallet - The developer earns passive revenue as more pools adopt the hook, creating an open-source-aligned incentive loop. --- ## Enable liquidity provision for verified users ### How can liquidity provisioning be opened to compliant participants? Just like swaps, liquidity provision can be enabled for KYC/AML-verified users. This ensures that approved participants can act as liquidity providers, ideal for institutional markets, real-world assets, or regulated jurisdictions. Hooks validate the LP’s address against a registry or attestation contract before accepting deposits. For example: # - A compliant stablecoin protocol allows verified users to provide liquidity in its `$RWA`/USDC pool. - When a user attempts to add liquidity, the hook checks an OFAC/KYC registry contract. - If the wallet is approved, liquidity is accepted. - The protocol maintains regulatory compliance while still using public blockchain infrastructure. --- ## Enable liquidity provision for specific character classes or levels ### How can games unlock liquidity access and tie rewards to in-game progression? Games can open liquidity provision to players with specific in-game attributes, like class, level, or faction, turning LP access into a gameplay achievement. Once unlocked, players who contribute liquidity can be rewarded with yield, in-game assets, XP, or other progression-linked benefits. This model keeps economic rewards thematic and gameplay-aligned, enhancing the sense of accomplishment while incentivizing meaningful contributions. For example: # - A blockchain RPG issues `$MANA`, the token used to craft and cast spells. - Wizard-class players at level 30+ can LP into the `$MANA`/ETH pool. - A LBAMM hook checks the character’s level before allowing liquidity to be added. - Whenever a swap uses that liquidity, the Wizard who provided it earns: - Spellcasting XP - Rare crafting materials - A leaderboard boost - Providing liquidity becomes a new endgame activity, tightly woven into the core of progression. --- ## Market Making & Price Control Market Making & Price Control patterns enable dynamic or oracle-based pricing, permissioned liquidity access, and programmable rules for price behavior. These mechanics give builders precision over how prices are discovered, maintained, and adapted for institutional DeFi, stable pricing, or regulated asset flows. ### What new opportunities do market making & price control unlock? - [Create liquidity pools with oracle-based pricing](#create-liquidity-pools-with-oracle-based-pricing) - [Enable liquidity access for verified traders](#enable-liquidity-access-for-verified-traders) - [Enable pool creation for accredited providers](#enable-pool-creation-for-accredited-providers) --- ## Create liquidity pools with oracle-based pricing ### How can pools use external pricing rather than reserves? Instead of using automated pricing based on reserves, Limit Break AMM allows liquidity pools to delegate pricing logic to external oracles. This enables pools to mirror off-chain or centralized market prices while still executing trust-minimized swaps. This is especially useful for RWA tokens, stablecoins, or synthetic assets. For example: # - A protocol tokenizes a barrel of oil as `$OIL`. - The `$OIL`/USDC pool doesn’t use a standard AMM curve instead it reads an oracle for real-world oil prices. - Swaps are executed at the current oracle price ± a configurable spread. - Users get real-world price execution with full on-chain auditability. --- ## Enable liquidity access for verified traders ### How can market makers connect directly with approved counterparties? Limit Break allows LPs to make their liquidity available only to verified traders. This is ideal for institutions, RFQ-style execution, or high-value trading strategies where counterparties are pre-approved. Hooks validate the swapper’s wallet or metadata before the LP’s liquidity is used. For example: # - An OTC desk provides $10M of ETH liquidity into a LBAMM pool. - Their liquidity position is protected by a hook that allows KYC’d counterparties to execute trades against it. - Approved traders benefit from direct access to deep liquidity. - Institutional LPs gain fine-grained control without needing custom infrastructure. --- ## Enable pool creation for accredited providers ### How can protocols empower accredited LPs to launch pools? For regulatory or compliance-sensitive environments, pool creation can be enabled exclusively for accredited providers. This ensures that entities meeting certain legal or financial standards can deploy new liquidity with confidence. Hooks can validate accreditation status via allowlists, attestation systems, or KYC/AML integrations before allowing new pools to be initialized. For example: # - A tokenized security protocol launches `$FUND`, representing shares in a regulated investment vehicle. - Accredited LPs can create new `$FUND`/USDC pools. - The token-level hook checks the caller’s address against an accreditation registry. - Approved providers can deploy liquidity while preserving regulatory compliance and onchain composability. # Audience & Roles This documentation serves multiple audiences with different goals and integration depths. Each section is designed to stand independently while sharing a common set of concepts and terminology. This page helps you identify which parts of the documentation are most relevant to you. ## Token Issuers **Who you are** You are deploying a new token or extending the behavior of an existing ERC-20 using LBAMM. You care about *where* and *how* your token can be used, and you want those constraints to hold consistently across liquidity venues. **What you’ll care about** * How token-level hooks work and when they are invoked * How pools, positions, and routes interact with token behavior * What guarantees LBAMM provides around execution context and enforcement * How much control you retain, and for how long **Recommended reading** * [Concepts & Terminology](./concepts-terminology) * [Protocol Fundamentals](../protocol-fundamentals) * [Token Hooks](../hooks/hook-types/token-hooks) * [Design Goals & Guarantees](./design-goals-guarantees) > **Common misconception:** LBAMM is only for Apptokens. > In reality, LBAMM works with any ERC-20 token. Apptokens are simply ERC-20s that are designed to fully leverage LBAMM’s execution guarantees and hook system. > **Common misconception:** Using LBAMM means permanent issuer control. > Control is defined by deployment choices. Hooks can be immutable, delegated, time-limited, or scoped. LBAMM enforces rules—it does not dictate who owns them or forever. --- ## DEX Integrators & Aggregators **Who you are** You are building a DEX, router, wallet, or aggregator that wants to access LBAMM liquidity or execute trades through LBAMM pools. You care about predictable execution semantics, composable interfaces, and avoiding edge-case failures when interacting with tokens that implement hook-based or policy-driven behavior. **What you’ll care about** * How execution context is constructed and propagated * What assumptions you can and cannot make about token behavior or hook side effects * How different pool types expose liquidity * How hooks affect swap, mint, burn, and transfer flows **Recommended reading** * [Protocol Fundamentals](../protocol-fundamentals) * [Pools Overview](../pools) * [Hook Call Order & Context](../hooks/hook-call-order-context) * [Integration Guides](../integration-guides) --- ## Hook Authors **Who you are** You are implementing custom logic that runs during LBAMM execution. You may be writing: * Token hooks * Pool hooks * Position-level constraints You care about correctness, call ordering, invariants, and minimizing unintended interactions. **What you’ll care about** * Exact hook interfaces and call signatures * When hooks run relative to core AMM logic * What execution context data is available * Which invariants the protocol guarantees vs. which you must enforce **Recommended reading** * [Hooks Overview](../hooks) * [Token Hooks](../hooks/hook-types/token-hooks) * [Pool Hooks](../hooks/hook-types/pool-hooks) * [Position Hooks](../hooks/hook-types/position-hooks) * [Protocol Fundamentals](../protocol-fundamentals) # Concepts & Terminology This page is the **canonical vocabulary** for the LBAMM docs. If a term is unclear later, refer back here. ## Core objects ### LBAMM core The protocol contract that: - Owns reserves and fee balances (pool types never custody reserves) - Constructs execution context - Invokes hooks in deterministic order - Performs token transfers and settlement - Enforces protocol-level bounds and invariants (fees, limits, atomicity) ### Pool A market between two tokens (`token0`, `token1`) with a specific **pool type** and optional **pool hook**. Pools are identified by a `poolId`. ### Pool type A contract that defines the liquidity and pricing mechanics for a pool, including: - Swap math - Liquidity accounting math - Position identifier derivation Pool types conform to a shared interface and execute under LBAMM core. Core remains the balance authority and settlement layer. ### Position A pool-type-defined representation of liquidity ownership/claims. - Every position belongs to exactly one pool. - The economic meaning of a position (ranges, bands, buckets, “height intervals”, etc.) is **defined by the pool type**. - Core treats positions as identifiers and enforces standardized settlement + hook validation. ### `poolId` A `bytes32` identifier for a pool. It is derived deterministically by the pool type (conceptually from `PoolCreationDetails`). ### `positionId` A `bytes32` identifier for a position. Pool types may derive it from core’s base ID plus pool-type-specific parameters. ### Base position identifier (AMM base position ID) A `bytes32` derived by core from: - `provider` (liquidity provider address), - `liquidityHook` (position hook address), - `poolId`. Because the **liquidity hook address is part of the identity**, integrators must persist and reuse it for subsequent operations on “the same” position. --- ## Execution model terms ### Execution A single **top-level call into LBAMM core** (e.g. `singleSwap`, `multiSwap`, `addLiquidity`, `removeLiquidity`, `collectFees`, flash loan). An execution: - Begins when the external call enters LBAMM core - Ends when that call returns (successfully or via revert) - Is **atomic**: if any required validation or hook invocation reverts, the entire execution reverts ### Execution context The non-spoofable metadata constructed by LBAMM core at the start of an execution and made available to hooks and pool types. Execution context explicitly encodes: - The **executor** (`msg.sender`) - The optional **recipient** (where relevant) - The hop index (for multi-hop swaps) - Operation-specific metadata required by the current action Execution context is shared across the entire execution and cannot be modified by hooks. ### Executor The **LBAMM caller** (`msg.sender`) for the current execution. - The executor is the authoritative identity exposed in execution context. - There is no `tx.origin` or “true initiator” concept in core semantics. > Integration implication: if an external router calls LBAMM, that router is the executor for hook-policy purposes. ### Recipient An address specified by certain operations (e.g. swaps) that receives assets as part of the execution. - May differ from the executor - Is surfaced in execution context where applicable ### Route, hop, and `hopIndex` - A **route** is an ordered sequence of hops (pools) in a `multiSwap`. - A **hop** is one pool interaction inside a route. - `hopIndex` is **zero-based** and increments per hop. ### Single-hop vs multi-hop - `singleSwap`: exactly one hop. - `multiSwap`: multiple hops with per-hop fees (LP + token hook swap fees), while **execution-level fees apply once**. --- ## Hook terminology ### Hook A contract invoked by LBAMM core at defined execution points to enforce or extend behavior. Hooks may be attached at different scopes: - Token hooks (token-global policy) - Pool hooks (pool-wide constraints) - Position hooks (position-scoped constraints) Hooks execute within the same execution context as core logic and may revert to block an operation. Hooks do **not**: - transfer tokens on behalf of core, - mutate core accounting directly, - partially commit an execution. ### Token hook A single hook configured **per token** via token settings. Token hooks can run on swaps, pool creation, liquidity ops, transfer-handler order validation, and flash loans (gated by flags). ### Pool hook A hook configured **per pool** at creation time. Pool hooks: - validate pool creation / liquidity ops, - may select **dynamic LP fee** at swap-time when dynamic fees are enabled. ### Position hook (liquidity hook) A hook configured **per position identity** (supplied in liquidity operation params). Used for position-scoped constraints like lockups/vesting/permissioning and for liquidity-operation hook fees. ### Hook flags / token settings A token’s hook behavior is gated by a compact bitmask in `packedSettings` set alongside `tokenHook`. - “Required” vs “supported” flags are negotiated via `hookFlags()` on the hook contract. - Only a subset of bits are consumed by core; remaining bits may be interpreted by the hook. ### `hookForToken0` / `hookForInputToken` Boolean indicators used in hook calls to disambiguate whether a hook is being invoked for: - the token0 vs token1 side (liquidity + pool creation), or - the input-token vs output-token side (swap hooks). ### Hook fees (general) Fees assessed by hooks (token/pool/position) that are: - accrued in core accounting, - typically collected later by the hook, - and may be **queued for settlement** after the primary operation finalizes. --- ## Swap and order terminology ### Swap order (taker intent) A struct describing “trade X for Y subject to limits” (deadline, tokens, amountSpecified, limitAmount, recipient, etc.). A swap order is executed either against pools or via direct settlement mechanisms. ### Swap-by-input vs swap-by-output - **Swap-by-input:** user specifies input amount; output is computed and bounded by a minimum/limit. - **Swap-by-output:** user specifies desired output amount; required input is computed and bounded by a maximum/limit. These modes affect fee ordering and caps. ### Direct swap A swap execution that does **not** interact with a pool. (Token swap hooks may still run; pool hooks do not, because no pool is touched.) ### Maker / taker - **Taker:** the party submitting an execution against existing liquidity/quotes. - **Maker:** the party providing liquidity or posting orders that takers consume. LBAMM supports both continuous maker liquidity (pools) and discrete maker liquidity (order/handler systems). --- ## Transfer handler terminology ### Transfer handler A settlement module invoked by LBAMM core during swap finalization to **supply the swap’s input tokens** to the AMM. - A handler is **not a router**. - It receives full swap context and can implement custom settlement flows (e.g., order books). ### `transferData` / `transferExtraData` Opaque bytes passed to a handler for handler-specific decoding/logic. ### `validateHandlerOrder` A token-hook validation function that is **not called by core**. Handlers may choose to call it during order creation/opening if token policy requires handler-level validation. --- ## Fee terminology (canonical) LBAMM has multiple fee surfaces. The key vocabulary is: ### Execution-level fees Fees passed to the top-level swap call and applied **once per execution**, not per hop: - **Exchange fee:** BPS-based fee on `tokenIn`, paid to an explicit recipient. - **Fee-on-top:** flat amount in `tokenIn`, paid to an explicit recipient. - **Executor fee:** represented as **fee-on-top** (no hidden executor reward surface). ### Hop-level fees Fees applied per hop inside a route: - **LP fee:** pool-level fee taken by the pool type (fixed or dynamically selected by pool hook). - **Token hook swap fees:** token-level fees assessed in `beforeSwap`/`afterSwap` per hop. - **Protocol hop minimum fees (if configured):** protocol-defined per-hop minimums. ### Fee ordering and settlement (high-level) - Execution-level fees apply once; hop-level fees apply per hop. In `multiSwap`, execution-level fees cannot compound across hops. - Hook-requested fee transfers are deferred/queued and settle after the primary operation finalizes. ### Fee bounds (terminology) - `maxHookFee0` / `maxHookFee1`: caller-supplied bounds that cap how much hook fees may be charged in liquidity operations. - Mode-specific fee caps exist for exchange fee BPS and (in swap-by-input) fee-on-top amount. --- ## Liquidity-operation terminology ### Add liquidity / remove liquidity / collect fees The three core liquidity operations. Core standardizes: - hook invocation ordering, - fee bounds enforcement, - settlement and accounting, while the pool type defines the mechanics and parameters. ### Provider The account performing the liquidity operation (the liquidity “actor” from core’s perspective). ### `poolParams` Pool-type-specific encoded parameters used to define the meaning of a liquidity operation (e.g., range bounds, “height interval” selection, etc.). Core does not interpret these. --- ## Failure and boundary terminology ### Revert vs “tokens owed” (debt) - If a hook reverts, the entire operation reverts (atomic policy enforcement). - Separately, some **failed token transfers** during liquidity settlement may be recorded as **tokens owed** rather than reverting (non-standard token behavior). This is distinct from hook failures and affects settlement/claim flows. ### Determinism A protocol goal for hook call ordering and context propagation: hooks and integrators should be able to reason about what runs, when, and with what metadata—especially across multi-hop execution. --- ## Related pages - [Protocol Fundamentals](../protocol-fundamentals) - [Hooks](../hooks) - [Hook Call Order & Context](../hooks/hook-call-order-context) - [Token Hooks](../hooks/hook-types/token-hooks) - [Pool Hooks](../hooks/hook-types/pool-hooks) - [Position Hooks](../hooks/hook-types/position-hooks) - [Liquidity Positions](../liquidity) - [Orders: Transfer Handlers](../orders/transfer-handlers) - [Fees: Fee Surfaces](../fees-economics/fee-surfaces) # Design Goals & Guarantees This page states the **protocol-level contract** LBAMM is designed to uphold: what it makes possible, what integrators can depend on, and what is explicitly not guaranteed. It is not a security analysis (see [Security Model](../security-model)), and it is not a replacement for detailed behavioral specifications in later sections. It defines the boundary of what you can safely build on. --- ## Design goals ### One execution model across all actions Swaps (single-hop, multi-hop, and direct), liquidity actions, pool creation, and flash loans share the same execution framing and context semantics. This makes policy reusable and predictable across “how value moves,” not just “which pool you touched.” ### Non-spoofable, minimal execution identities LBAMM keeps execution identities minimal and explicit: - **Executor** is the LBAMM caller (`msg.sender`). - **Recipient** is surfaced where relevant (e.g. swaps). ### Policy is explicit, composable, and scoped Policy attaches at the correct layer: - **Token hooks** for token-global guarantees - **Pool hooks** for venue/pool-wide constraints - **Position hooks** for position-scoped constraints (lockups, vesting, operator flows, tranche rules) LBAMM enforces the *execution framework*; hooks define the policy. This is “policy as first-class,” but still opt-in via configuration choices. ### Liquidity mechanics are modular without breaking enforcement Pool types define liquidity/pricing mechanics, but the enforcement model and execution semantics remain stable across pool types. New market designs should not require reinventing how context and policy are enforced. ### Fee surfaces are explicit and bounded LBAMM is designed so integrators can reason about “what you pay”: - **Execution-level** fees apply once per execution (even in multi-hop). - **Hop-level** fees apply per hop. - Hook fees are bounded in liquidity flows and are settled in a controlled way. --- ## Protocol-level guarantees From the design goals above, the following properties hold: ### Single execution context per top-level call Every top-level call into LBAMM core constructs exactly one execution context that is shared across all pools, hooks, and internal logic for the duration of the call. ### Atomicity If any required validation, hook invocation, or invariant check reverts, the entire execution reverts. No partial state transitions persist. ### Deterministic hook invocation surface Hooks are invoked at defined execution points with a consistent context model. Core does not conditionally skip hook surfaces based on routing path. ### Core-enforced settlement authority LBAMM core owns reserves and performs settlement. Pool types and hooks do not custody reserves independently. ### Policy attachment at defined scopes Policy attaches only at defined surfaces: - Token scope - Pool scope - Position scope Core enforces the execution framework; hooks define policy within that framework. --- ## Explicit non-goals and non-guarantees LBAMM intentionally does not guarantee: ### Economic outcomes LBAMM does not guarantee profitability, fair pricing, liquidity depth, or protection against adverse market conditions. ### Hook safety Hooks are external contracts. LBAMM guarantees invocation surfaces, not the correctness, gas efficiency, or safety of hook implementations. ### Cross-protocol enforcement LBAMM enforces policy within its own execution model. It does not enforce behavior in external AMMs, bridges, or off-protocol transfers. ### Absence of revert Hooks may revert based on custom policy. Integrators must assume that any operation may revert if policy conditions are not satisfied. ### Backward compatibility of custom pool types While core execution semantics are stable, custom pool types must conform to interface expectations. Incorrect implementations can violate expected economic behavior. --- ## Trust boundaries (who you are trusting) LBAMM’s modularity is a power tool. It also means **risk is delegated**. - **Core AMM:** trusted for custody, accounting, context construction, and enforcement of bounds/invariants. - **Token hooks:** trusted by anyone interacting with that token (token-global policy surface). - **Pool hooks:** trusted by pool creators and pool participants (venue-wide policy surface). - **Position hooks:** trusted by position providers (position-scoped policy surface). - **Pool types:** trusted by pool creators/LPs for liquidity math and position semantics; core remains the balance authority. (See [Security Model](../security-model) for the full risk surface taxonomy.) --- ## Practical guidance for integrators ### If you are building a router / aggregator - Assume **you** are the executor (hooks will see you as `msg.sender`). - Prefer designs that keep LBAMM as the top-level call when possible to avoid executor-policy conflicts. - Treat hook reverts as expected behavior; build fallbacks and quote-time simulation accordingly. ### If you are integrating non-trivial tokens - Read token hook configuration first; it defines what must be enforced. - Assume token behavior can affect swaps, liquidity actions, and order settlement. ### If you are authoring hooks - Treat your hook as part of the protocol’s trust surface: document revert conditions, fee behavior, and any external dependencies. - Keep work bounded and deterministic; design for multi-hop composability. --- ## Related pages - [Concepts & Terminology](./concepts-terminology) - [Protocol Fundamentals](../protocol-fundamentals) - [Hooks Overview](../hooks) - [Fee Surfaces](../fees-economics/fee-surfaces) - [Position Policy Enforcement](../liquidity/position-policy-enforcement) - [Security Model](../security-model) # What is LBAMM? The **Limit Break AMM (LBAMM)** is a **modular AMM framework designed for Apptokens** that supports multiple pool types and liquidity models, while allowing **token, pool, and position behavior to be extended or constrained through programmable hooks**. At its core, LBAMM separates *liquidity mechanics* from *token- and application-level rules*. Rather than embedding assumptions about token behavior directly into a single AMM design, LBAMM provides a common execution layer where rules can be attached explicitly and enforced consistently. This makes it possible to build DEX infrastructure that: * Enforces token-level constraints **across venues and routes** * Executes trades with a **reliable, non-spoofable execution context** * Exposes **simple, composable interfaces** for integrators and hook authors ## Why LBAMM exists Traditional AMMs assume that tokens are passive ERC‑20 balances and that all policy lives *outside* the pool. In practice, this creates gaps: * Token-level rules can often be bypassed by routing through unexpected venues or liquidity sources * Execution context (who is executing the trade, who receives outputs, and how the route is constructed) is difficult or impossible to reason about reliably * Extending behavior requires forking pool logic rather than composing reusable constraints LBAMM addresses these problems by making **execution context explicit** and giving **token/pool/position policy** a first-class enforcement surface. Instead of asking every pool or integrator to re‑implement the same checks, LBAMM provides a shared framework where: * Token behavior can be enforced wherever liquidity exists * Pools focus on pricing and liquidity mechanics * Hooks define *when* and *how* behavior is constrained or extended ## Apptokens and ERC‑20 compatibility LBAMM is designed to work with **any ERC‑20 token**, not just Apptokens. Apptokens are a *specialized class* of ERC‑20s that are built to take full advantage of LBAMM’s execution guarantees and hook system. However: * Non‑Apptoken ERC‑20s can be traded, pooled, and routed through LBAMM * Pools and hooks do not require tokens to be Apptokens to function Apptokens are **not a requirement** to use LBAMM — they are simply the tokens that can express the richest behavior within it. ## Control, mutability, and issuer intent A common misconception is that LBAMM gives token issuers permanent or unilateral control. In reality, LBAMM supports a **spectrum of control models**, defined at deployment time: * Issuers may deploy hooks that are fully immutable * Issuers may delegate control or governance to other parties * Issuers may time‑limit or scope enforcement to specific pools or positions The framework does not mandate *who* controls behavior or *for how long* — it only ensures that whatever rules exist are enforced consistently. ## Who this documentation is for This documentation is written for three primary audiences: * **Token issuers** building Apptokens or attaching policies to existing ERC‑20s * **DEX integrators and aggregators** integrating LBAMM pools, routes, and execution flows * **Hook authors** implementing custom token, pool, or position behavior Each section of the docs is structured so that readers can go as deep as necessary, from high‑level mental models to low‑level integration details. ## Next steps If you are new to LBAMM, read these in order: 1. [Audience & Roles](./audience) 2. [Concepts & Terminology](./concepts-terminology) 3. [Protocol Fundamentals](../protocol-fundamentals) 4. [Design Goals & Guarantees](./design-goals-guarantees) These pages establish the shared language and core execution model used throughout the rest of the docs. # Pool Hooks Pool hooks allow pool-specific policy and dynamic behavior to be expressed **per pool**, without forking pool math or modifying the core AMM. A pool hook is configured at pool creation time via `PoolCreationDetails.poolHook` and becomes part of the pool’s stored state. This page documents: * How pool hooks are configured * Which pool hook functions can be invoked and when * Dynamic LP fee selection via the pool hook * What pool hooks are allowed to do (and what they are not) For hook ordering relative to token and position hooks, see the call-order reference. --- ## Configuration and lifecycle ### Attachment A pool hook is specified during pool creation: * `PoolCreationDetails.poolHook` The pool state stores the hook address: ```solidity struct PoolState { address token0; address token1; address poolHook; uint128 reserve0; uint128 reserve1; uint128 feeBalance0; uint128 feeBalance1; } ``` ### Mutability Pool hooks are configured once at creation time and are not updated afterward. ### Scope Pool hooks apply only to operations that interact with a pool: * Pool creation * Liquidity operations on that pool * Swap fee selection for that pool when dynamic fees are enabled Pool hooks are **not** invoked for `directSwap` (no pool interaction). --- ## Pool hook interface A pool hook implements `ILimitBreakAMMPoolHook` and may receive calls for: * Pool creation validation * Dynamic pool fee selection (optional, only for dynamic-fee pools) * Liquidity operation validation and per-operation fee assessment Notably, pool hooks are not used as general before/after swap validators. The only swap-time pool hook invocation is for **dynamic pool fee selection**. --- ## When pool hooks run ### Pool creation A pool hook is validated when a pool is created *with that hook address*: * `validatePoolCreation(poolId, creator, details, hookData)` The hook **must revert** to block pool creation. --- ### Swaps: dynamic LP fees only Pool hooks participate in swaps only when the pool was created with the **dynamic fee sentinel**. In that case, the AMM calls: * `getPoolFeeForSwap(context, poolFeeParams, hookData)` If a pool was created with a fixed LP fee, the pool hook is not invoked during swaps. #### Dynamic fee sentinel To opt into hook-driven dynamic LP fees, the pool must be created with a sentinel LP fee value: ```solidity uint16 constant DYNAMIC_POOL_FEE_BPS = 55_555; ``` When this sentinel value is used, the pool hook returns the effective fee rate (in basis points) for the swap. #### Fee bounds enforced by core The AMM enforces strict upper bounds on the returned fee: * Input-based swaps: `poolFeeBPS <= 10_000` * Output-based swaps: `poolFeeBPS < 10_000` Pool hooks may compute fees based on any available context, including executor, recipient, hop index, tokens, pool, and swap direction. --- ### Liquidity operations Pool hooks may validate and assess fees during liquidity operations: * `validatePoolAddLiquidity(...)` * `validatePoolRemoveLiquidity(...)` * `validatePoolCollectFees(...)` These functions: * **must revert** to block the operation * may return `hookFee0` and `hookFee1` to charge additional fees to the provider Fees returned by pool hooks are allocated to the **hook** in the AMM and must be collected by the hook. --- ## Context available to pool hooks Pool hooks reuse the same core context types used across the protocol: * `LiquidityContext` for liquidity operations (provider, token pair, positionId) * `LiquidityModificationParams` / `LiquidityCollectFeesParams` for operation parameters * `SwapContext` and `HookPoolFeeParams` for dynamic fee selection Pool hooks therefore have enough information to: * Restrict who may access a pool for liquidity provision * Restrict or validate certain swap activity (when expressed via fee selection and/or pool creation / liquidity constraints) * Implement dynamic fees conditioned on execution metadata --- ## What pool hooks can do ### Enforce pool-scoped policy Pool hooks commonly enforce pool-scoped rules such as: * Who may add/remove liquidity * Pool access control (allow/deny specific providers) * Constraints tied to pool parameters or pool identity Pool hooks may also influence swap activity indirectly by: * Rejecting pool creation for undesirable configurations * Restricting liquidity provisioning * Returning dynamic LP fees that depend on execution context ### Assess hook fees on liquidity operations Pool hooks may charge additional fees from the liquidity provider by returning `hookFee0` and/or `hookFee1` during liquidity operations. ### Request fee transfers (queued) During liquidity hook execution, pool hooks may call into the AMM to request fee transfers. * Requests are queued during execution * Settled after the liquidity operation finalizes * Processed FIFO --- ## What pool hooks cannot do Pool hooks do not replace pool math or core accounting. Pool hooks must not assume they can: * Modify pool reserves or fee balances directly * Perform token transfers on behalf of the AMM * Change swap parameters (path, recipient, amounts) * Validate arbitrary swap execution via before/after swap callbacks (except dynamic fee selection when enabled) --- ## Failure semantics Pool hook calls are atomic with the operation: * If any pool hook reverts, the entire operation reverts. This applies to pool creation, liquidity operations, and dynamic fee selection. # Position Hooks Position hooks (liquidity hooks) allow **position-scoped policy** to be enforced for liquidity operations. Unlike token hooks (global per token) and pool hooks (scoped per pool), a position hook is part of the identity of a position and is applied to: * Adding liquidity * Removing liquidity * Collecting LP fees This page documents: * How position hooks attach to positions * Which functions can be invoked and when * What context position hooks receive * What position hooks are allowed to do (and what they are not) --- ## What is a position hook? A position hook is a contract implementing `ILimitBreakAMMLiquidityHook` that validates liquidity operations for a position. Position hooks are designed for constraints such as: * Lockups / vesting / time-based restrictions * Enforcing that liquidity providers use a specific hook implementation * Additional fee logic scoped to a position Position hooks do **not** change who owns a position. --- ## Attachment and identity ### Supplied via liquidity operation params A position hook is specified via: * `LiquidityModificationParams.liquidityHook` (add/remove liquidity) * `LiquidityCollectFeesParams.liquidityHook` (collect fees) A zero address indicates no position hook. ### Part of the base position identifier The core AMM derives a base position identifier that includes the position hook address. Conceptually: ```solidity bytes32 basePositionId = keccak256( abi.encode( provider, liquidityHook, poolId ) ); ``` Because `liquidityHook` is included: * The hook is *bound* to the position’s identity. * Liquidity operations for that position must use the same hook address to resolve the same base position. Pool types may derive a final `positionId` from the base ID and pool-type parameters. > Position hooks observe only the final `positionId`, not the base position ID. --- ## When position hooks run Position hooks are invoked once per liquidity operation: * `validatePositionAddLiquidity(...)` * `validatePositionRemoveLiquidity(...)` * `validatePositionCollectFees(...)` Position hooks are not invoked for: * swaps * pool creation * flash loans --- ## Position hook interface A position hook implements the following validation functions: * `validatePositionAddLiquidity(...)` * `validatePositionRemoveLiquidity(...)` * `validatePositionCollectFees(...)` Each function: * **must revert** to block the operation * may return `hookFee0` and `hookFee1` to charge additional fees to the provider --- ## Context available to position hooks Position hooks receive: * `LiquidityContext` with: * `provider` * `token0`, `token1` * `positionId` (final, pool-type-defined) * Liquidity operation parameters: * `LiquidityModificationParams` or `LiquidityCollectFeesParams` Key points: * `provider` is the account performing the liquidity operation (the `msg.sender` of the AMM call). * Position hooks receive `poolId` (via liquidity params), but do not receive the pool hook address directly. * Position hooks can observe fee bounds such as `maxHookFee0` and `maxHookFee1` from liquidity params. --- ## Fees and fee requests ### Per-operation hook fees Position hooks may charge additional fees by returning: * `hookFee0` (denominated in token0) * `hookFee1` (denominated in token1) Fees returned by position hooks are allocated to the **hook** in the AMM and must be collected by the hook. ### Requesting fee transfers (queued) During hook execution, position hooks may call into the AMM to request fee transfers (e.g., `collectHookFeesByHook`). Fee transfer requests are: * queued during execution * settled after the liquidity operation finalizes * processed FIFO --- ## What position hooks can do Position hooks are intended for position-scoped constraints, such as: * Locking liquidity until a timestamp or vesting condition is met * Enforcing that only certain providers may modify a position * Enforcing that a specific hook implementation is used for a class of positions Token hooks may also require that liquidity providers use specific position hooks for a token. --- ## What position hooks cannot do Position hooks do not replace core accounting or pool-type position semantics. Position hooks must not assume they can: * Change position ownership * Modify pool reserves or fee balances directly * Perform token transfers on behalf of the AMM * Change liquidity operation parameters * Partially commit execution (operations remain atomic) --- ## Failure semantics Position hook calls are atomic with the liquidity operation: * If any position hook reverts, the entire liquidity operation reverts. # Token Hooks Token hooks are the mechanism LBAMM uses to enforce **token-level policy** across all venues, pool types, and execution paths. A token hook is a single contract configured per token. When enabled, LBAMM invokes the hook at well-defined points (swaps, liquidity operations, pool creation, flash loans, and transfer-handler order validation). This page documents: * How token hooks are configured * Which hook functions can be invoked and when * What context token hooks receive * What token hooks are allowed to do (and what they are not) For the exact invocation ordering relative to pool/position hooks, see the call-order reference. --- ## Configuration: `setTokenSettings` Token hooks are configured per token via: ```solidity setTokenSettings(address token, address tokenHook, uint32 packedSettings) ``` ### Authorization `setTokenSettings` may only be called by an authorized party for the token: * The token owner * A role-based admin * An account holding `LBAMM_TOKEN_SETTINGS_MANAGER_ROLE` ### Enabling and disabling * Tokens default to `tokenHook = address(0)` (no token hook). * A token hook may be removed by setting `tokenHook = address(0)`. * When `tokenHook` is zero, `packedSettings` **must** be zero. ### Flag negotiation: `hookFlags()` A token hook declares its required and supported behavior via: ```solidity function hookFlags() external view returns (uint32 requiredFlags, uint32 supportedFlags); ``` * **requiredFlags** are flags the token owner **must** enable for the hook to function correctly. * **supportedFlags** are flags the token owner **may** enable. * All required flags must be supported. LBAMM validates the configuration at `setTokenSettings` time: * If the token owner enables an unsupported flag → the call reverts. * If the token owner fails to enable a required flag → the call reverts. Only 10 bits of the 32-bit `packedSettings` value are currently consumed by LBAMM for hook gating. The remaining 22 bits are stored by core and may be used by hook developers for token-specific semantics. > The core stores `packedSettings` and makes it retrievable to hooks (e.g. via `getTokenSettings(token)`). --- ## Hook gating flags The protocol uses the following flag bits in `packedSettings` to determine which token hook functions are invoked: | Bit | Constant | Enables | | --- | --------------------------------------------- | --------------------------------------------------------------------- | | 0 | `TOKEN_SETTINGS_BEFORE_SWAP_HOOK_FLAG` | Enables `beforeSwap` hook validation | | 1 | `TOKEN_SETTINGS_AFTER_SWAP_HOOK_FLAG` | Enables `afterSwap` hook validation | | 2 | `TOKEN_SETTINGS_ADD_LIQUIDITY_HOOK_FLAG` | Enables hook validation during `addLiquidity` | | 3 | `TOKEN_SETTINGS_REMOVE_LIQUIDITY_HOOK_FLAG` | Enables hook validation during `removeLiquidity` | | 4 | `TOKEN_SETTINGS_COLLECT_FEES_HOOK_FLAG` | Enables hook validation during `collectFees` | | 5 | `TOKEN_SETTINGS_POOL_CREATION_HOOK_FLAG` | Enables hook validation during `createPool` | | 6 | `TOKEN_SETTINGS_HOOK_MANAGES_FEES_FLAG` | Indicates hook contract manages its own fee collection | | 7 | `TOKEN_SETTINGS_FLASHLOANS_FLAG` | Enables flash loan operations for the token | | 8 | `TOKEN_SETTINGS_FLASHLOANS_VALIDATE_FEE_FLAG` | Enables validation when flash loan fees are paid in a different token | | 9 | `TOKEN_SETTINGS_HANDLER_ORDER_VALIDATE_FLAG` | Enables transfer handler order validation | These flags are evaluated by the core AMM before invoking the corresponding hook functions. --- ## Token hook interface A token hook implements `ILimitBreakAMMTokenHook` and may receive calls for the following operations: * Pool creation validation * Swap validation and per-swap fee assessment * Liquidity operation validation and per-operation fee assessment * Transfer-handler order validation * Flash loan validation and optional cross-token fee validation The hook must revert to block an operation. --- ## When token hooks run Token hooks run for **any token involved in the operation**, subject to flags. ### Pool creation For pool creation, token hooks may be called for: * `token0` * `token1` Hook function: * `validatePoolCreation(...)` The hook receives: * `creator` (the account calling the AMM to create the pool) * `hookForToken0` indicating whether the invocation is for the token0 or token1 side * `PoolCreationDetails` containing pool type, fee, token pair, pool hook, and pool params ### Swaps (single, multi-hop, direct) For each hop of a swap, token hooks may be called for: * the hop’s input token * the hop’s output token Hook functions: * `beforeSwap(...)` (if enabled) * `afterSwap(...)` (if enabled) Both hooks return a **fee amount** assessed by the AMM. The fee denomination depends on swap direction and whether the hook is executed before or after the swap. ### Liquidity operations For liquidity operations, token hooks may be called for: * `token0` * `token1` Hook functions: * `validateAddLiquidity(...)` * `validateRemoveLiquidity(...)` * `validateCollectFees(...)` These validation functions may return `hookFee0` and `hookFee1`, allowing a hook to charge fees in either or both pool tokens. ### Transfer-handler orders Token hooks may validate transfer-handler orders via: * `validateHandlerOrder(...)` This hook is not called by the AMM core directly. It is invoked by transfer handlers during order creation. ### Flash loans Flash loans involve two possible token-hook interactions: * `beforeFlashloan(...)` on the **loan token** (gated by `TOKEN_SETTINGS_FLASHLOANS_FLAG`) * Authorizes the loan * Returns `(feeToken, feeAmount)` * `validateFlashloanFee(...)` on the **fee token** when a different token is used to pay the fee (gated by `TOKEN_SETTINGS_FLASHLOANS_VALIDATE_FEE_FLAG`) * Validates whether the fee token may be used --- ## Context available to token hooks Token hooks receive rich, non-spoofable context from the AMM core. ### Swap context Swap hooks receive: * `SwapContext` (who is executing, who receives output, fee recipients, transfer handler, and swap token pair) * `HookSwapParams` (swap direction, hop index, poolId, amounts, and whether this call is for tokenIn vs tokenOut) Key points: * **Executor** is the authoritative identity (`msg.sender` of the AMM call). There is no `tx.origin`/initiator field. * **Recipient** may differ from executor and is intentionally surfaced so hooks can restrict executor→recipient flows. * **Hop index** is zero-based; for `multiSwap` it increments monotonically per hop (and will be zero for a single-hop swap). * `transferHandler` may be present to settle a swap. ### Liquidity context Liquidity hooks receive: * `LiquidityContext` including `provider`, the pool token pair, and the resolved `positionId` Token hooks also observe the liquidity hook address via: * `LiquidityModificationParams.liquidityHook` * `LiquidityCollectFeesParams.liquidityHook` This allows token hooks to validate which position hook is being used. --- ## What token hooks can do Token hooks are designed to express policy and dynamic behavior. ### Enforce policy by reverting All token hook validation functions **must revert** to block an operation. Common uses include: * Permissioning (who may trade, provide liquidity, or receive tokens) * Execution-path restrictions (e.g., only certain executors) * Slippage and parameter validation * KYC/AML enforcement ### Assess hook fees Token hooks may assess additional fees: * `beforeSwap` / `afterSwap` return a fee amount for swap flows * Liquidity validation functions may return `hookFee0` / `hookFee1` Fee allocation depends on token settings. In particular, `TOKEN_SETTINGS_HOOK_MANAGES_FEES_FLAG` indicates that the hook contract manages its own fee collection. ### Request fee transfers (queued) Token hooks may call into the AMM during hook execution to request fee transfers via: * `collectHookFeesByHook` Fee transfer requests are: * queued during execution * settled after the primary operation finalizes * processed in FIFO order This prevents fee settlement from interfering with swap or liquidity finalization. --- ## What token hooks cannot do Token hooks are **not** a substitute for core accounting. Token hooks must not assume they can: * Perform or override token transfers performed by the AMM * Modify pool reserves or fee balances directly * Change swap parameters (path, recipient, amounts) * Partially commit execution (all-or-nothing execution still applies) Hooks *can* run arbitrary logic within the EVM (including interacting with other contracts), but transfer and accounting semantics remain controlled by the AMM core. --- ## Failure semantics Token hook calls are atomic with execution: * If any token hook reverts, the entire operation reverts. This is true for swaps, pool creation, liquidity operations, and flash loans. --- ## Related pages * Hook call ordering and execution context (reference) * Hook economics (fees, settlement, and accounting) * Pool hooks * Position hooks # AMM Standard Token Hook The **AMM Standard Token Hook** is a first-party token hook intended for token issuers who want **simple, issuer-controlled constraints** on how their token can be used inside LBAMM (swaps, pool creation, liquidity adds). It is composed of two contracts: 1. **Creator Hook Settings Registry** — the *source of truth* and issuer-facing configuration surface. 2. **AMM Standard Hook** — the token hook address set on LBAMM core, which **enforces** settings during execution and maintains local caches updated by the registry. This page documents **how to use** the hook and what it enforces. For ABI-level details, see: - Reference → Core Interfaces (Registry + Hook) - Reference → Contract Registry (canonical addresses) --- ## Mental model ### LBAMM core is hook-agnostic LBAMM core does not “understand” issuer settings beyond: - which token hook to call, and - which hook flags are enabled for that token. All issuer policy lives in the **AMM Standard Hook** (enforcement) and the **Settings Registry** (configuration). ### Registry is the configuration system of record Token issuers set policy in the registry. The registry then **pushes** updates to one or more hook implementations (including the AMM Standard Hook) via explicit sync calls. ### Hook switching is a first-class workflow Issuers can switch token hooks in the future. The intended path is: 1) configure settings in the registry (and sync them to the new hook), 2) then point LBAMM core at the new token hook. --- ## Who can administer settings? A caller may update a token’s registry settings if they are: - the token contract itself, or - the token contract `owner()` (ERC-173/Ownable-style), or - the token’s `DEFAULT_ADMIN_ROLE` (AccessControl-style) > The registry is canonical (one per chain). --- ## Setup and lifecycle ### Initial setup (enable the AMM Standard Hook) **Step A — configure the registry** Call the registry to set your `HookTokenSettings` and push them to the AMM Standard Hook. Conceptually: - `registry.setTokenSettings(token, settings, ..., hooksToSync=[ammStandardHook])` **Step B — enable on LBAMM core** Call `setTokenSettings` on LBAMM core to set: - the token hook address = AMM Standard Hook - the token’s packed settings bits (hook flags you want enabled) > Important: if no relevant hook flags are enabled for a token, the hook is effectively **inert** (none of its enforcement runs). ### Updating settings after launch To update issuer policy: 1) update settings in the registry, and push updates to the AMM Standard Hook via `hooksToSync` 2) if you need to change which hook callbacks run, update LBAMM core token settings bits (hook flags) ### Switching hooks safely Recommended order: 1) Update registry settings and push/sync them to the **new** hook implementation 2) Call LBAMM core `setTokenSettings` to switch tokenHook to the new hook address --- ## What the hook enforces The AMM Standard Hook enforces different constraints depending on which hook functions are enabled via core flags. ### beforeSwap `beforeSwap` may: - **Block swaps** when: - the pool is disabled (per-pool disable flag is set by either token) - trading is paused (global pause for the token) - direct swaps are blocked (`blockDirectSwaps = true`) - a direct swap uses a pairing token not on the paired-token whitelist (if configured) - the pool price is outside the configured price bounds (if set), *unless the swap “heals” the price* (see Price Bounds) - **Apply token-imposed swap fees** Fees are computed from the hook’s settings using the swap direction and whether the hook is being invoked for the input token or output token side. ### afterSwap `afterSwap` repeats many of the same validity checks and may also apply fees. Why both? Two reasons: - some constraints depend on post-swap price (“did the swap move price back into bounds?”) - fee logic may depend on amounts that are only final after swap fill ### validateHandlerOrder (handler-controlled) This function is **not called by LBAMM core**. A transfer handler may call it (e.g., at order creation time) if it wants the token’s hook to validate an order’s implied price. AMM Standard Hook behavior: - If price bounds are configured, it validates the order is within bounds using `amountIn`/`amountOut` by deriving the implied `sqrtPriceX96`. ### validateAddLiquidity May block add-liquidity when: - the pool is disabled - an LP whitelist is configured and `provider` is not whitelisted - the pool price is outside bounds (with the same “healing” behavior where applicable) ### validatePoolCreation May block pool creation when: - a pool type whitelist is configured and the pool type is not allowed - the LP fee rate is outside `[minFeeAmount, maxFeeAmount]` - the paired token is not allowed (paired-token whitelist) - initial pool price is outside configured bounds (if bounds are set) - an LP whitelist is configured and the pool creator is not whitelisted ### beforeFlashloan If enabled, this hook **always reverts**. Practically: - enabling the beforeFlashloan flag = “disable flashloans for this token” ### validateFlashloanFee If enabled, this hook **always reverts**. Practically: - enabling validateFlashloanFee = “disallow using this token as the fee-payment token for flashloans” --- ## Swap fee semantics The AMM Standard Hook’s swap fee selection uses two axes: - **Mode:** `inputSwap` (swap-by-input) vs output-based - **Side:** whether the hook is invoked for the **input token** (`hookForInputToken = true`) or the other side It selects one of four fee settings: - `tokenFeeBuyBPS`: Fee rate assessed on token when buying (paired token in, token out) - `tokenFeeSellBPS`: Fee rate assessed on token when selling (token in, paired token out) - `pairedFeeBuyBPS`: Fee rate assessed on paired token when buying (paired token in, token out) - `pairedFeeSellBPS`: Fee rate assessed on paired token when selling (token in, paired token out) ### Where fees accrue and how they are collected AMM Standard Hook does **not** manage fees itself. Fees accrue to the token and are claimable via LBAMM core: * `collectHookFeesByToken(tokenFor, tokenFee, recipient, amount)` Parameters: - `tokenFor` - Address of the token that fees belong to. - `tokenFee` - Address of the token the fees are denominated in. - `recipient` - Address to receive the fees. - `amount` - Amount of fees to transfer to `recipient`. Only the token contract, owner, admin, or an account assigned the `LBAMM_TOKEN_FEE_COLLECTOR_ROLE` may collect. --- ## Pool disabling vs trading pause These are different controls with different blast radius: ### Disable a specific pool Registry: `setPoolDisabled(token, poolId, disable)` * Either token in the pool can disable it. * A pool is treated as disabled if **either** token has disabled it. * If both tokens disable, both must re-enable. Effect: * trading in that pool is blocked * liquidity withdrawal remains possible (so “disable pool” is not a hostage mechanism) The registry verifies `token` is one of the pool’s tokens by checking pool state in LBAMM core. ### Pause trading globally for a token Registry: set `tradingIsPaused = true` in `HookTokenSettings` Effect: * blocks trading across all pools involving this token * still allows liquidity add/remove/fee collection (i.e., it is a *trading* pause, not a full freeze) --- ## Price bounds Price bounds are configured per (token, pairToken): * `(minSqrtPriceX96, maxSqrtPriceX96)` in Q96 sqrt price space * bounds may be unset by setting both min/max to 0 ### Enforcement phases The hook checks bounds: * **pre-swap** and **post-swap** * during pool creation (initial price) * during add-liquidity validation * for handler order validation (derived from `amountIn/amountOut`) ### “Healing” behavior If the current price is out of bounds, the hook may still allow a swap **if it moves price toward the configured range** (i.e., a swap that “heals” an out-of-range market). ### Multiple allowed pair tokens If you allow multiple pair tokens, bounds are configured independently per pair token. Issuers should set bounds for every pair token they want enforced. --- ## Whitelists and cache synchronization The registry manages list ownership + membership. Lists are referenced from `HookTokenSettings` by ID: * `poolTypeWhitelistId` * `pairedTokenWhitelistId` * `lpWhitelistId` `0` means “no restriction”. ### Important: settings sync vs membership sync When you call `setTokenSettings(..., hooksToSync=...)`, the registry syncs the **HookTokenSettings struct** (including list IDs) to the hook, but it does **not** necessarily sync the full membership content of referenced lists. Membership updates are synced to hooks via explicit calls when you update lists. For example: * `updatePairTokenWhitelist(...)` calls `hook.registryUpdateWhitelistPairToken(listId, tokens, add)` * `updatePoolTypeWhitelist(...)` calls `hook.registryUpdateWhitelistPoolType(listId, poolTypes, add)` * `updateLpWhitelist(...)` calls `hook.registryUpdateWhitelistLpAddress(listId, accounts, add)` ### Behavior on stale caches The hook enforces based on its **known** whitelist state. If you depend on strict allow/deny behavior, make list updates and hook sync part of your operational playbook. --- ## Settings reference (HookTokenSettings) * `initialized`: registry has initialized settings for this token * `tradingIsPaused`: block trading for this token across pools * `blockDirectSwaps`: prevent `directSwap` paths involving this token * `checkDisabledPools`: enforce per-pool disable checks against registry state * `tokenFeeBuyBPS` / `tokenFeeSellBPS`: fees applied depending on direction/side * `pairedFeeBuyBPS` / `pairedFeeSellBPS`: fees applied depending on direction/side * `minFeeAmount` / `maxFeeAmount`: bounds on **LP fee rate** at pool creation * `poolTypeWhitelistId`: restrict pool types allowed for this token * `pairedTokenWhitelistId`: restrict pairing tokens allowed * `lpWhitelistId`: restrict liquidity providers / pool creators (where enforced) --- ## Common integration implications ### Routers become the executor If a router calls LBAMM, hooks see the router as `executor`. Tokens may restrict execution based on that identity. Design your integration accordingly. ### Direct swaps are not “always available” If `blockDirectSwaps` is enabled, `directSwap` paths will revert for this token. ### Bounds are in sqrtPriceX96 (Q96) Bounds are expressed in sqrt price format; ensure your issuer tooling and UI present this safely. # Hook Call Ordering & Context This page defines the deterministic hook invocation model for LBAMM. For each top-level execution type, it specifies: - Which hooks are invoked - In what order they are invoked - What execution context is visible at each stage - How reverts propagate All hook invocation occurs within a single execution context constructed by LBAMM core. If any required hook invocation reverts, the entire execution reverts. --- ## Shared execution context principles All hook calls occur within a single LBAMM **execution** (a single top-level AMM call). * The authoritative identity is the **executor** (`msg.sender` of the AMM call). * There is no `tx.origin` / initiator field. * Hooks may observe a **recipient** where applicable (e.g. swaps). * Hook-triggered fee transfers are **queued** during execution and settled after the primary operation finalizes. --- ## Ordering: high-level phases Hook calls are grouped into phases depending on the operation: * Pool creation * Swaps (single-hop, multi-hop) * Liquidity operations (add/remove/collect) * Flash loans Within each operation, LBAMM uses a consistent ordering across hook categories. --- ## Pool creation ordering When creating a pool, the core may invoke: * Token hooks for `token0` and `token1` (if `TOKEN_SETTINGS_POOL_CREATION_HOOK_FLAG` is enabled for the token) * The pool hook specified by `PoolCreationDetails.poolHook` (if non-zero) **Canonical order:** 1. Token0 hook: `validatePoolCreation(..., hookForToken0=true, ...)` 2. Token1 hook: `validatePoolCreation(..., hookForToken0=false, ...)` 3. Pool hook: `validatePoolCreation(...)` If any hook reverts, pool creation reverts. --- ## Swap ordering Swaps may be: * Single-hop (one pool) * Multi-hop (multiple pools) * Direct swap (no pool) ### Direct swaps A direct swap has **no pool interaction**. * Token swap hooks may run (subject to token settings flags) * Pool hooks do not run * Position hooks do not run ### Pool-based swaps For each hop in a swap, token hooks may be invoked for both the hop’s input and output token (subject to flags). Token hooks are invoked in two phases: * `beforeSwap` (if `TOKEN_SETTINGS_BEFORE_SWAP_HOOK_FLAG` enabled) * `afterSwap` (if `TOKEN_SETTINGS_AFTER_SWAP_HOOK_FLAG` enabled) **Per-hop canonical order:** 1. Token hook(s) — beforeSwap * Token In hook: `beforeSwap(... hookForInputToken=true ...)` * Token Out hook: `beforeSwap(... hookForInputToken=false ...)` 2. (Optional) Pool hook dynamic fee selection * Only if the pool was created with `DYNAMIC_POOL_FEE_BPS` (`55_555`) * Calls `getPoolFeeForSwap(context, poolFeeParams, hookData)` 3. Pool-type swap execution (pricing + reserve/fee accounting) 4. Token hook(s) — afterSwap * Token In hook: `afterSwap(... hookForInputToken=true ...)` * Token Out hook: `afterSwap(... hookForInputToken=false ...)` If any hook reverts, the swap reverts. > **Note:** `HookSwapParams.hopIndex` is zero-based. It increments monotonically for `multiSwap` and is `0` for a single-hop swap. ### Fee settlement during swaps Token hooks may request hook-fee transfers during `beforeSwap`/`afterSwap`. * Requests are queued during execution * Swap hops complete * Input/output token movements finalize * Queued fee requests settle afterward (FIFO) --- ## Liquidity operation ordering Liquidity operations include: * `addLiquidity` * `removeLiquidity` * `collectFees` All three operations share the same high-level ordering across hook categories. **Canonical order:** 1. Pool type execution of liquidity operation 2. Token hook for token0 then token1 2. Position hook (liquidity hook), if non-zero 3. Pool hook, if non-zero ### Token hook invocation For liquidity operations, token hooks may be invoked for both `token0` and `token1` of the pool (subject to flags): * Add liquidity: `TOKEN_SETTINGS_ADD_LIQUIDITY_HOOK_FLAG` * Remove liquidity: `TOKEN_SETTINGS_REMOVE_LIQUIDITY_HOOK_FLAG` * Collect fees: `TOKEN_SETTINGS_COLLECT_FEES_HOOK_FLAG` Token hook functions: * `validateAddLiquidity(...)` * `validateRemoveLiquidity(...)` * `validateCollectFees(...)` Token hook invocations include a `hookForToken0` boolean to indicate whether the call is being performed for the token0 or token1 side. ### Position hook invocation If `liquidityHook != address(0)`, the AMM invokes the position hook: * `validatePositionAddLiquidity(...)` * `validatePositionRemoveLiquidity(...)` * `validatePositionCollectFees(...)` ### Pool hook invocation If `poolHook != address(0)`, the AMM invokes the pool hook: * `validatePoolAddLiquidity(...)` * `validatePoolRemoveLiquidity(...)` * `validatePoolCollectFees(...)` ### Fee settlement during liquidity operations Token, position, and pool hooks may request hook-fee transfers during validation. * Requests are queued during execution * The liquidity operation finalizes (reserves/fee balances updated; provider net amounts processed) * Queued fee requests settle afterward (FIFO) --- ## Flash loan ordering Flash loans are token-hook-governed: 1. Loan-token hook `beforeFlashloan(...)` (gated by `TOKEN_SETTINGS_FLASHLOANS_FLAG`) 2. (Optional) Fee-token hook `validateFlashloanFee(...)` if a different fee currency is specified (gated by `TOKEN_SETTINGS_FLASHLOANS_VALIDATE_FEE_FLAG`) Pool and position hooks do not participate in flash loans. --- ## Context objects This section summarizes the primary context objects used by hooks. ### SwapContext `SwapContext` includes: * `executor` * `transferHandler` * exchange-fee recipient and BPS * fee-on-top recipient and amount * `recipient` * `tokenIn`, `tokenOut` * `numberOfHops` ### HookSwapParams `HookSwapParams` includes: * `inputSwap` (true for input-based swaps, false for output-based swaps) * `hopIndex` * `poolId` * `tokenIn`, `tokenOut` * `amount` (interpreted based on `inputSwap`) * `hookForInputToken` ### LiquidityContext `LiquidityContext` includes: * `provider` (the account performing the liquidity operation) * `token0`, `token1` * `positionId` (final, pool-type-defined) ### LiquidityModificationParams and LiquidityCollectFeesParams Liquidity parameter structs include: * `poolId` * `liquidityHook` * bounds for net amounts and hook fees (e.g., `maxHookFee0`, `maxHookFee1`) * pool-type-specific `poolParams` --- ## Notes for integrators * Hook calls are part of execution atomicity: a revert in any hook reverts the operation. * Hooks do not move value directly; any hook-fee transfers requested during execution are settled after the primary operation finalizes. * The executor is always the AMM caller. If you insert an external router, it becomes the executor for hook purposes. --- ## Related pages - [Hooks Overview](../hooks) - [Token Hooks](./hook-types/token-hooks) - [Pool Hooks](./hook-types/pool-hooks) - [Position Hooks](./hook-types/position-hooks) - [Protocol Fundamentals](../protocol-fundamentals) - [Writing Safe Hooks](./writing-safe-hooks) # Hooks Hooks are contracts invoked by LBAMM core at defined execution points to enforce or extend behavior. They execute within the same execution context as core logic and may revert to block an operation. Hooks do not own reserves or perform settlement; they operate within the execution framework enforced by LBAMM core. Hooks are the primary extension mechanism in LBAMM. They allow token issuers, pool designers, and integrators to enforce policy and inject dynamic behavior **without modifying core AMM logic**. Hooks are attached at defined scopes: - Token scope (token-global guarantees) - Pool scope (venue-level constraints) - Position scope (liquidity-position constraints) The scope determines when a hook runs and what invariants it may enforce. Hook invocation timing and context details are specified in [Hook Call Order & Context](./hook-call-order-context). --- ## What is a hook? A **hook** is a contract that is invoked by the LBAMM core at well-defined points during execution. Hooks may: * Validate whether an operation is allowed * Enforce permissions or policy * Supply dynamic attributes (such as fees or pricing) * Request fee transfers Hooks **do not**: * Perform token transfers * Update pool reserves directly * Override execution atomicity All value movement and accounting is handled by the LBAMM core. --- ## Hook types LBAMM supports multiple hook categories that attach at different layers of the protocol. Each category exists to scope responsibility and observability appropriately. At a high level, hooks may attach at the **[token](./hook-types/token-hooks)**, **[pool](./hook-types/pool-hooks)**, or **[position](./hook-types/position-hooks)** level. The precise interfaces, invocation rules, and validation semantics for each hook type are documented in the corresponding hook-type pages. This page focuses only on the shared model and guarantees that apply to *all* hooks, regardless of category. --- ### Pool hooks **Attachment:** Per pool Pool hooks are specified at pool creation time via: * `PoolCreationDetails.poolHook` Pool hooks observe and validate pool-specific behavior, such as: * Swap conditions * Liquidity constraints * Pool-specific invariants --- ### Position hooks **Attachment:** Per position Position hooks are supplied during liquidity operations via: * `LiquidityModificationParams.liquidityHook` * `LiquidityCollectFeesParams.liquidityHook` Position hooks are optional. A zero address indicates no position hook. They are primarily used to enforce rules around **who may modify or collect from a given position**. --- ## Hook enablement model Hooks are invoked only when explicitly enabled by configuration. * Token-level hooks are gated by a compact **settings bitmask** supplied at configuration time * Pool and position hooks are attached explicitly at pool creation or liquidity modification The protocol enforces compatibility between hooks and their configuration so that unsupported or undefined hook behavior cannot be invoked. Detailed flag definitions and per-operation behavior are documented in the token hook reference. --- ## Execution context observability All hooks are invoked with non-spoofable execution context supplied by the LBAMM core. At a minimum, hooks can reliably observe: * The **executor** of the execution * The action being performed (swap, liquidity modification, fee collection, flash loan) * Relevant token and pool identifiers Additional context (such as hop index, recipients, or position identifiers) is provided where applicable and is documented per hook type. --- ## Responsibilities and boundaries Hooks exist to express **policy and dynamic behavior**, not to replace core protocol logic. ### What hooks are for Hooks are commonly used for: * Permissioning and access control * Validation of execution parameters * Dynamic attributes such as fees or pricing inputs ### What hooks are not for Hooks must not assume responsibility for: * Token transfers * Reserve or fee accounting * Partial or best-effort execution All value movement and state updates are performed by the LBAMM core. --- ## Hook-requested fees Hooks may request fee transfers as part of execution. * Fee requests are **deferred** and settled after the primary operation completes * Fee settlement is performed by the LBAMM core The mechanics and authorization rules for hook-requested fees are documented in the relevant hook-type sections. --- ## Failure semantics Hook execution is fully atomic with respect to execution: * If **any hook reverts**, the entire execution reverts * Hooks cannot partially succeed This guarantees that validation and policy enforcement cannot be bypassed by downstream behavior. --- ## Why hooks are central to LBAMM By making hooks explicit, scoped, and declarative, LBAMM enables: * Token-level enforcement across all liquidity venues * Pool-agnostic policy and validation * Clean separation between accounting, pricing, and control logic Subsequent sections document each hook type and its interfaces in detail. --- ## For hook authors If you are implementing a hook contract, read: - [Writing Safe Hooks](./writing-safe-hooks) - [Hook Call Order & Context](./hook-call-order-context) # Writing Safe Hooks This page documents the engineering constraints hook authors must follow to remain safe and composable within LBAMM’s execution model. Hooks execute inside a single atomic execution context. If a hook reverts, the entire execution reverts. Improper hook design can break integrations, cause unexpected reverts, or introduce economic inconsistencies. Read [Hook Call Order & Context](./hook-call-order-context) before implementing a hook. --- ## Understand what you are allowed to call During hook execution, the only **state-changing** AMM call that hooks are intended to perform is: * `collectHookFeesByHook(...)` Hooks may call back into the AMM for **view** functions. This constraint is intentional: * It preserves execution atomicity * It prevents hooks from mutating pool state mid-validation * It reduces reentrancy and state-inconsistency risk > If you need additional stateful interactions, design them as part of the main execution flow rather than invoking them from inside hook callbacks. --- ## Fee-side semantics for token swap hooks Token swap hooks (`beforeSwap` and `afterSwap`) return a fee amount. The token the fee is denominated in depends on: * whether the swap is **input-based** (`swapByInput`) or **output-based** (`swapByOutput`) * whether the hook is **beforeSwap** or **afterSwap** ### Fee denomination truth table | Swap type | `beforeSwap` fee is denominated in | `afterSwap` fee is denominated in | | ------------ | ---------------------------------- | --------------------------------- | | swapByInput | tokenIn | tokenOut | | swapByOutput | tokenOut | tokenIn | This is a common source of mistakes. If a hook assumes the wrong side, it may: * apply fees to an unintended token * cause swaps to revert due to insufficient available amounts * mis-estimate net settlement for the executor/recipient --- ## Required vs supported flags Token hooks declare configuration capabilities via: * `requiredFlags` — flags that must be enabled * `supportedFlags` — flags that may be enabled A practical pattern: * Mark **before/after** swap hooks as supported but not required when your logic needs both *in some cases*, but can safely operate with only one in others. This avoids forcing token owners into enabling callbacks they do not need, while still allowing advanced enforcement when desired. --- ## Avoid fee overconsumption across hooks Multiple hooks can assess fees during a single execution (token hooks, pool hooks, and position hooks). If fees collectively overconsume the available input/output amounts, operations may revert. Guidelines: * Keep hook fees bounded and predictable * Use user-supplied bounds where available (e.g., `maxHookFee0/maxHookFee1` on liquidity ops) * Consider how fees stack across: * token hook swap fees * pool/position hook liquidity fees * queued hook-fee transfer requests --- ## Liquidity operations and transfer failures For liquidity operations, LBAMM may record failed token transfers as **debt** rather than reverting. Important boundaries: * This applies only when a **token transfer fails** during a liquidity operation. * It is distinct from hook reverts: * If a hook reverts, the entire operation reverts. Hook authors should avoid assuming that passing validation implies transfers will succeed for non-standard ERC-20s. (Details of the debt mechanism and its accounting are documented in the liquidity operation reference.) --- ## Keep hooks cheap and deterministic The protocol does not force a specific gas budget for hooks, but hook authors should assume: * Hooks run on the hot path of swaps and liquidity operations * Expensive hooks will degrade UX and can become a practical DoS vector Practical guidance: * Avoid unbounded loops and dynamic per-user iteration * Treat external calls as potentially fragile and expensive * Prefer constant-time checks and compact calldata formats # Custom Pool Types Custom pool types define the liquidity model, pricing logic, and position accounting rules for a pool. LBAMM separates responsibilities clearly: Core: - Holds reserves and fee balances - Executes hooks - Transfers tokens - Enforces invariants - Validates protocol fees Pool Types: - Implement swap math - Implement liquidity math - Manage pool-specific state - Derive position identifiers Pool types never custody reserves. Core is always the balance authority. --- ## Pool Type Interface All pool types *must* implement [ILimitBreakAMMPoolType](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMPoolType.sol). --- # Core vs Pool Type Responsibilities ## Core Guarantees Core exclusively manages: - reserve0 / reserve1 - feeBalance0 / feeBalance1 - Protocol fee storage - Hook execution ordering - Token transfers - Partial fill rules - Swap invariant validation Pool types must never attempt to transfer tokens or maintain shadow reserve balances. --- ## Pool Type Responsibilities Pool types are responsible for: - Deterministic swap calculation - Deterministic liquidity accounting - Position ID derivation - Internal state management - Optional dynamic pricing logic - Returning correct LP and protocol fee amounts Pool types are stateful contracts. They may: - Store internal liquidity model state - Store oracle observations - Adjust pricing based on volatility - Decode and use swapExtraData - Read view functions from the AMM core They may not mutate core storage. --- # Pool ID Encoding poolId is a packed value: - Bits 0 – 15 → Pool fee in BPS - Bits 16 – 143 → Creation details hash - Bits 144 – 255 → Pool type address (must have 6 leading zero bytes) Requirements: - Pool type address must be encoded in upper 112 bits - Fee must be encoded in lower 16 bits - Creation hash must ensure deterministic uniqueness Multiple pools may share the same token pair but differ by: - Pool type - Fee - Pool parameters --- # Swap Model Core determines swap direction and applies hook logic. Pool types compute swap outputs. Input Swap: - May partially fill only on first hop - Must return actualAmountIn, amountOut, feeOfAmountIn, protocolFees - Must ensure total fees do not exceed amountIn Output Swap: - Must return actualAmountOut, amountIn, feeOfAmountIn, protocolFees - Requires poolFeeBPS < 10000 - 10000 BPS would zero the effective input and break reverse calculation Core validates: - Total fee bounds - Protocol minimum enforcement - Reserve safety - Dynamic fee bounds --- # SwapContext Pool types receive contextual information including: - Executor identity - Transfer handler - Exchange fee configuration - Fee-on-top configuration - Final recipient - Hop count Pool types may use this for: - Context-aware pricing - Compliance-aware liquidity - Oracle adjustments - Executor-dependent logic Or ignore it entirely. --- # Liquidity & Position Model Core derives a stable base position ID from: provider, liquidityHook, and poolId. Pool types must deterministically derive positionId from: - ammBasePositionId - poolParams - Internal rules The same derivation must succeed for: - addLiquidity - removeLiquidity - collectFees --- # Dynamic Fees If pool fee equals the dynamic sentinel value, Core queries the pool hook for the fee. Dynamic fee must: - Be deterministic relative to state - Be strictly less than 10000 BPS - Preserve swap invariants --- # Protocol Fee Storage Single swap: - Accumulates protocol fee in memory - Stored once at finalization Multi-hop: - Stored per hop - Required because input token changes --- # getCurrentPriceX96 Allows integrations to query price without reproducing pool logic. The pool type may: - Compute from internal state - Derive from reserves - Incorporate oracle adjustments --- # poolTypeManifestUri Each pool type exposes a manifest URI describing: - Liquidity model - Swap model - Required poolParams - Required swapExtraData - Dynamic fee behavior - Position derivation rules If the URI changes, the pool type must emit: PoolTypeManifestUriUpdated(string uri) --- # Design Philosophy LBAMM separates: - Execution math (pool type) - Settlement authority (core) Core is the settlement layer. Pool types are deterministic execution modules. # Pools This page defines what a **pool** is in LBAMM, how pools are identified, and how different liquidity models fit into a shared execution and accounting framework. The goal of this page is to establish **what is invariant across all pools** versus **what is delegated to pool types**, without diving into pool-specific math. --- ## What is a pool? In LBAMM, a **pool** is a stateful onchain object that: * Defines how two tokens are priced and exchanged * Holds reserves and accrued LP fees * Delegates liquidity accounting and swap math to a specific **pool type** Every pool is uniquely identified by a `bytes32 poolId`, which is the **universal handle** used throughout the protocol. ### Pool identifiers A `poolId` is generated by the pool type contract at creation time, subject to constraints enforced by the LBAMM core: * **Upper 14 bytes**: the pool type address * Pool type addresses are required to begin with 6 leading zero bytes * **Lower 2 bytes**: the LP fee rate * **Middle 16 bytes**: pool-type-defined data * Intended to encode *static, non-storage-based parameters* for the pool * Must both uniquely identify the pool instance and capture any parameters the pool type needs to reconstruct pricing behavior without additional storage This structure allows LBAMM to: * Recover the pool type directly from the `poolId` * Enforce fee-rate constraints uniformly * Leave flexibility to pool types to define their own uniqueness rules The pool type address can be derived directly from the `poolId`: ```solidity address poolType = address(uint160(uint256(poolId >> 144))); ``` ### Token ordering For all AMM pools: * Token ordering is canonicalized such that `token0 < token1` * If tokens are supplied out of order, the core AMM will flip them during pool creation This guarantees consistent pool identity and simplifies downstream accounting. --- ## Pool state vs pool logic LBAMM deliberately separates **shared state** from **pool-specific logic**. ### Core AMM responsibilities The core AMM tracks, enforces, and updates: * Total reserves for each token in the pool * Accrued LP fee balances * Invariants preventing swaps or withdrawals in excess of available reserves The core AMM does **not**: * Understand liquidity position structure * Compute prices or swap curves * Interpret pool-type-specific parameters ### Pool type responsibilities Each pool type defines: * How liquidity positions are represented * How swaps are priced and executed * How reserves and fees map onto positions * Which liquidity models and invariants apply Pool types are responsible for maintaining the correctness of their liquidity model; the core AMM enforces only global reserve and fee-balance safety. --- ## Required pool type interface All pool types must support a common set of capabilities in order to integrate with LBAMM: * Pool creation * Swaps by input amount * Swaps by output amount * Add and remove liquidity * LP fee collection * View functions for: * Current price * Pool ID computation for given parameters * A manifest URI describing pool capabilities This uniform interface allows different liquidity models to coexist while remaining composable at the execution layer. > **Note:** Direct swaps are *not* a pool type. They execute without any pool interaction and are documented separately. --- ## Pool types LBAMM supports multiple pool types, each defining its own liquidity model and pricing behavior. This section intentionally avoids detailed descriptions of individual pool types. Each pool type is documented on its own page, where its mechanics, invariants, and intended use cases are specified precisely. At a fundamentals level, all pool types: * Implement the common pool type interface * Operate under the same execution and accounting guarantees * Delegate policy decisions to hooks where applicable Developers may deploy additional pool types permissionlessly, provided they conform to the required interface. --- * All liquidity and fees are owned by a single account * Swap pricing is delegated to the pool hook This pool type is useful for RFQ-style markets or application-controlled pricing. --- ## Fees and fee surfaces From the perspective of a pool type: * Pools always take fees as a percentage of `tokenIn` * The LP fee rate is encoded in the `poolId` ### Fixed vs dynamic LP fees * Pools may use a **fixed BPS fee** specified at creation * Pools may opt into **dynamic fees** via a pool hook To enable dynamic fees, the pool must be created with a sentinel LP fee value: ```solidity /// @dev Sentinel value indicating dynamic fee calculation should be used instead of fixed fee uint16 constant DYNAMIC_POOL_FEE_BPS = 55_555; ``` When this sentinel is used, the pool hook is responsible for determining the effective fee for each swap. ### Swap-level fees Other fees—such as exchange fees or flat fees with custom recipients—are **swap-level parameters**, not pool-level concerns. Pool types are agnostic to these fees; they only observe the net input amount after such fees are applied. --- ## Hooks and pools Pool types and hooks have clearly separated roles: * **Pool types** define pricing, liquidity accounting, and model-specific invariants * **Hooks** define validation and dynamic behavior, such as: * Whether an account may add liquidity * What fee rate applies to a specific swap This separation ensures that: * Pool math remains self-contained and auditable * Policy and permissions remain explicit and programmable --- ## Positions (high-level) Liquidity positions are **pool-type-defined**, not core-AMM-defined. At a high level: * Positions are created through the core AMM * A base position identifier and encoded pool-type-specific parameters are supplied * The pool type may derive a final position ID (e.g. by hashing base ID + parameters) The core AMM: * Does not store position data * Only updates reserves and fee balances based on pool type instructions All position semantics—ranges, ownership, accounting—are delegated to the pool type. --- ## Why this structure matters By standardizing pool identity and execution while delegating liquidity mechanics, LBAMM enables: * Multiple liquidity models to coexist safely * Token- and pool-level policy to be enforced consistently * Permissionless experimentation with new pool designs Subsequent sections document each pool type and its position model in detail. # Mixing Pool Types in Routes LBAMM routes can be **multi-hop**: a single swap can pass through multiple pools, and those pools may be **different pool types**. This page is for **routers and aggregators** building multi-hop routes across LBAMM pools. --- ## Core idea A multi-hop route is a sequence of independent pool executions: - Each hop executes according to **that pool type’s swap mechanics** - The route as a whole is one LBAMM execution, but **pool math does not “blend” across hops** If you mix pool types in a route, the only thing that “connects” them is the **output amount from hop _i_ becoming the input amount to hop _i+1_**. --- ## What changes when you mix pool types When routes mix pool types, integrators must treat each hop as having its own mechanics, including: - How price is determined (e.g., state-driven vs hook-supplied) - How liquidity is represented and consumed (e.g., reserves vs active liquidity over ranges) - Whether partial fills are possible and under what conditions - What extra per-hop parameters are required (e.g., price limits in extra data) - How quoting should be performed for that pool type (what must be simulated vs what can be assumed) **There is no universal quoting shortcut.** A valid route quote must respect the mechanics of every hop. --- ## What stays the same Across all pool types, the route is still “just hops”: - A hop specifies a `poolId`, `tokenIn`, `tokenOut`, and an amount (input-based or output-based depending on the swap path). - The result of a hop is an amount of `tokenOut` that becomes the next hop’s `tokenIn`. --- ## Example: multi-hop route across multiple pool types Imagine you want to swap `TOKEN_A` → `TOKEN_D` using a 3-hop route: 1. **Dynamic Price Pool**: `TOKEN_A` → `TOKEN_B` 2. **Fixed Price Pool**: `TOKEN_B` → `TOKEN_C` 3. **Single Provider Pool**: `TOKEN_C` → `TOKEN_D` Conceptually: - Hop 1 produces `TOKEN_B` according to the Dynamic Price Pool’s price movement and liquidity-range mechanics. - Hop 2 consumes that exact `TOKEN_B` output and produces `TOKEN_C` using the Fixed Price Pool’s fixed execution price and reserve constraints. - Hop 3 consumes that `TOKEN_C` output and produces `TOKEN_D` using the Single Provider Pool’s hook-supplied execution price and core reserve availability. Even though the route is one operation, each hop must be quoted and simulated under the correct assumptions for that pool type. --- ## Practical guidance for routers - Treat each hop as a separate pricing engine. - Build quoting logic that can evaluate each pool type’s swap semantics. - When constructing multi-hop routes, ensure the per-hop inputs you supply (amounts, limits, extra data) are valid for that pool type. That’s the whole model: **multi-hop routing is composition of per-hop mechanics, not a new liquidity model.** # Pool Abstraction LBAMM supports heterogeneous pool types under a unified execution and enforcement framework. At a high level: - **AMM Core** owns token custody, hook execution, and invariant enforcement. - **Pool Types** provide pricing logic, position logic, and pool-specific state. - **Pools** are identified by a deterministic `poolId` that encodes key parameters. This page defines the contract boundary between core and pool types. --- ## What a Pool Is A pool in LBAMM consists of: - `token0` and `token1` (canonical ordering enforced at creation) - A `poolType` contract (execution logic module) - A pool fee (fixed BPS or dynamic sentinel) - An optional `poolHook` - Deterministic `poolId` Multiple pools may share the same token pair. They are distinguished by: - `poolType` - `fee` - `poolHook` - `poolParams` (pool-type-specific initialization data) --- ## Core vs Pool Type Responsibilities ### Core Responsibilities Core: - Custodies reserves - Tracks LP fee balances - Tracks protocol fees - Executes token, position, and pool hooks - Performs all token transfers - Enforces invariants on returned values Core is invariant-aware. --- ### Pool Type Responsibilities Pool types: - Implement pricing and liquidity logic - Maintain pool-specific state - Compute swap outcomes - Compute LP and protocol fee amounts - Return deterministic outputs to core Pool types: - **Do not transfer tokens** - **Do not custody reserves** - **Do not mutate core accounting** They return values. Core settles. --- ## Core Pool State Core stores a minimal, pool-agnostic structure: ```solidity struct PoolState { address token0; address token1; address poolHook; uint128 reserve0; uint128 reserve1; uint128 feeBalance0; uint128 feeBalance1; } ``` Meaning: - `reserve0` / `reserve1` are canonical reserves. - `feeBalance0` / `feeBalance1` represent unclaimed **LP fees only**. - Protocol fees are stored separately. - `poolHook` is immutable after creation. Pool types may maintain additional internal state keyed by `poolId`. --- ## poolId Encoding `poolId` is deterministic and bit-packed. ```solidity /// poolId is a packed value of the pool type address, /// hash of creation details, and pool-specific packed data. /// /// Bits 0 to 15 - pool fee in BPS. /// Bits 16 to 143 - creation details hash (may include pool-specific params). /// Bits 144 to 255 - pool type address (must have 6 leading zero bytes). ``` Constraints: - Pool type addresses must have 6 leading zero bytes. - Encoded pool type must match `details.poolType`. - Encoded fee must match `details.fee`. Core validates both during creation. --- ## PoolDecoder Utilities Core extracts encoded components using `PoolDecoder`: ```solidity library PoolDecoder { function getPoolType(bytes32 poolId) internal pure returns (address poolType) { assembly ("memory-safe") { poolType := shr(POOL_ID_TYPE_ADDRESS_SHIFT, poolId) } } function getPoolFee(bytes32 poolId) internal pure returns (uint16 fee) { fee = uint16(uint256(poolId) >> POOL_ID_FEE_SHIFT); } } ``` `poolId` is the canonical routing and indexing key. --- ## Pool Creation Creation parameters: ```solidity struct PoolCreationDetails { address poolType; uint16 fee; address token0; address token1; address poolHook; bytes poolParams; } ``` Creation flow: 1. Validate token contracts. 2. Enforce canonical ordering (`token0 < token1`). 3. Validate fee: - ≤ 10,000 BPS - or dynamic sentinel 4. Delegate to pool type: ```solidity poolId = poolType.createPool(details); ``` 5. Validate returned `poolId` encoding. 6. Store `PoolState`. 7. Execute token and pool creation hooks. 8. Emit `PoolCreated`. Event: ```solidity event PoolCreated( address indexed poolType, address indexed token0, address indexed token1, bytes32 poolId, address poolHook ); ``` --- ## Dynamic Fee Sentinel Dynamic fee pools use: ```solidity uint16 constant DYNAMIC_POOL_FEE_BPS = 55_555; ``` If this sentinel is used: - A non-zero `poolHook` must be provided. - The pool hook participates in dynamic fee calculation. --- ## Pool Type Interface All pool types *must* implement [ILimitBreakAMMPoolType](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMPoolType.sol). --- ## Swap Boundary Guarantees For swaps: - Pool type returns computed amounts. - Core validates LP fee and protocol fee correctness. - Core updates reserves. - Core performs token transfers. - Pool types never move tokens directly. Multi-hop swaps are decomposed by core into sequential `swapByInput` or `swapByOutput` calls per hop. --- ## Read Surfaces ### getCurrentPriceX96 ```solidity function getCurrentPriceX96(address amm, bytes32 poolId) external view returns (uint160 sqrtPriceX96); ``` Provides a standardized price read surface across heterogeneous pool types. ### poolTypeManifestUri ```solidity function poolTypeManifestUri() external view returns (string memory manifestUri); ``` Provides integration metadata for applications and SDKs. If updated, pool types must emit: ```solidity event PoolTypeManifestUriUpdated(string uri); ``` --- ## Determinism Model Pool types may: - Use oracle accumulators - Use volatility metrics - Use time-dependent state As long as: - Outputs are deterministic given onchain state and inputs - Returned values are consistent with encoded fee rates - Invariants on reserves and fees are respected Core enforces accounting correctness. --- ## Summary LBAMM separates: - **Execution & custody** (core) - **Pricing & liquidity logic** (pool types) `poolId` encodes: - Pool type - Fee - Creation parameters This architecture enables heterogeneous liquidity models to coexist safely under unified invariant enforcement. # Dynamic Price Pool The **Dynamic Price Pool** is a concentrated-liquidity pool type where liquidity is provided over **tick ranges** and the **execution price moves during swaps** as liquidity is consumed across ranges. This page documents **integration-relevant mechanics** only: 1. Liquidity model (ticks, ranges, active liquidity) 2. Pool creation parameters 3. Position lifecycle (add → remove → collect fees) 4. Fee behavior and how it differs from “owed-fees” collection patterns 5. Snap-to-price behavior (`snapSqrtPriceX96`) 6. Swap behavior (input/output paths, price limits, partial fills) 7. Events and indexing guidance This page is written for integrators interacting with **LBAMM Core**. --- # Key Integration Concept: AMM-Partitioned State Dynamic Price Pool is implemented as **reusable middleware**. The pool type contract stores pool and position state in storage **partitioned by the calling AMM**, where: - `amm = msg.sender` That means: - Multiple AMM-core deployments that implement the LBAMM pool-type interface can all use the **same** Dynamic Price pool-type contract address. - Pool and position state does **not** collide across AMMs because it is stored under a per-AMM partition (`globalState[amm]`). - The same `poolId` value can exist under multiple AMMs (identical parameters → identical `poolId`), but it refers to **different pools** in different AMM partitions. **Integrator takeaway:** a `poolId` must always be interpreted alongside the AMM address it belongs to. --- # Liquidity Model ## Ticks and ranges Liquidity is provided over a range: - `tickLower` (inclusive) - `tickUpper` (exclusive) Ticks must satisfy: - `tickLower < tickUpper` - both ticks are within global tick bounds - both ticks align to the pool’s `tickSpacing` The pool maintains: - `sqrtPriceX96` (current price as sqrt(token1/token0) * 2^96) - `tick` (the active tick for the current price) - `liquidity` (active liquidity at the current tick) ## Price representation Price is represented as: - `sqrtPriceX96 = sqrt(token1 / token0) * 2^96` This aligns with LBAMM’s standardized `getCurrentPriceX96` read surface for pool types. --- # Creating a Dynamic Price Pool Pools are created via LBAMM core by calling `createPool` with: - `PoolCreationDetails.poolType` set to the Dynamic Price Pool type address. - `PoolCreationDetails.poolParams` set to the ABI-encoded bytes of `DynamicPoolCreationDetails`. ## DynamicPoolCreationDetails ```solidity /** * @dev Parameters specific to a dynamic pool creation. * @param tickSpacing Tick spacing. * @param sqrtPriceRatioX96 Initial price ratio as sqrt(price) * 2^96 */ struct DynamicPoolCreationDetails { int24 tickSpacing; uint160 sqrtPriceRatioX96; } ``` ### Field semantics - `tickSpacing` - Granularity of valid ticks. - All positions must use ticks aligned to this spacing. - Also included in the `poolId` derivation. - `sqrtPriceRatioX96` - Initial pool price at creation. - Must be within supported bounds (see constants below). - Used to initialize the pool state: - `sqrtPriceX96` - `tick = TickMath.getTickAtSqrtPrice(sqrtPriceRatioX96)` - `liquidity = 0` ### Bounds and constants ```solidity /// @dev Minimum tick value for concentrated liquidity positions, equivalent to price of ~1e-38 int24 constant MIN_TICK = -887_272; /// @dev Maximum tick value for concentrated liquidity positions, equivalent to price of ~1e38 int24 constant MAX_TICK = 887_272; /// @dev Tick spacing must be at least 1. int24 constant MIN_TICK_SPACING = 1; /// @dev Tick spacing is capped at 16384 to prevent overflow in tick traversal helpers. int24 constant MAX_TICK_SPACING = 16_384; /// @dev Equivalent to getSqrtPriceAtTick(MIN_TICK) uint160 constant MIN_SQRT_RATIO = 4_295_128_739; /// @dev Equivalent to getSqrtPriceAtTick(MAX_TICK) uint160 constant MAX_SQRT_RATIO = 1_461_446_703_485_210_103_287_273_052_203_988_822_378_723_970_342; ``` ## Pool identity The `poolId` is deterministic and includes: - pool type address - LP fee (BPS) - tick spacing - token0, token1 - pool hook address This means you can deploy multiple pools for the same token pair that differ by fee, tick spacing, or hook. ## Example: createPool ```solidity DynamicPoolCreationDetails memory dynamicDetails = DynamicPoolCreationDetails({ tickSpacing: 60, sqrtPriceRatioX96: initialSqrtPriceX96 }); PoolCreationDetails memory details = PoolCreationDetails({ poolType: DYNAMIC_POOL_TYPE, fee: 30, // 0.30% token0: token0, token1: token1, poolHook: address(0), poolParams: abi.encode(dynamicDetails) }); (bytes32 poolId,,) = lbamm.createPool( details, "", "", "", "" ); ``` --- # Positions and Position IDs LBAMM core derives a stable `ammBasePositionId` from: - `provider` (the liquidity provider) - `liquidityHook` - `poolId` Dynamic Price Pool derives the final `positionId` by combining: - `ammBasePositionId` - `tickLower` - `tickUpper` Conceptually: - same provider + same hook + same pool + same range ⇒ same position **Liquidity hooks are common** in this pool type. Using different `liquidityHook` addresses results in distinct base IDs and therefore distinct positions, even for the same provider and tick range. --- # Liquidity Operations Dynamic Price Pool supports: - `addLiquidity` - `removeLiquidity` - `collectFees` Unlike many router patterns, liquidity modification is specified directly in **liquidity units**. ## DynamicLiquidityModificationParams Used for `addLiquidity` and `removeLiquidity`. ```solidity /** * @dev Parameters for modifying dynamic pool liquidity. * * @param tickLower Lower bound of the liquidity range * @param tickUpper Upper bound of the liquidity range * @param liquidityChange Amount of liquidity to add (positive) or remove (negative) * @param snapSqrtPriceX96 Price to move to prior to adding liquidity (optional) */ struct DynamicLiquidityModificationParams { int24 tickLower; int24 tickUpper; int128 liquidityChange; uint160 snapSqrtPriceX96; } ``` ### Field semantics - `tickLower`, `tickUpper` - Define the position range. - Must be ordered, within bounds, and aligned to tick spacing. - `liquidityChange` - The **net change in liquidity**, not token amounts. - For `addLiquidity`, must be **\>= 0**. - For `removeLiquidity`, must be **\<= 0**. - `0` is allowed and acts like an expensive “collect-on-modify” operation (it still runs the modification path and fee realization rules). - `snapSqrtPriceX96` - Optional. - Only applied during `addLiquidity`. - If non-zero, attempts to move the pool price to the snap price *before* adding liquidity, subject to strict constraints (see Section 6). ### What amounts mean in LBAMM For liquidity modification operations, the pool type returns: - token amounts required to deposit (for add) - token amounts to withdraw (for remove) - fees realized during the operation LBAMM core performs the token movements and enforces user-supplied bounds (`min/max` fields in core liquidity params). ## Example: addLiquidity ```solidity DynamicLiquidityModificationParams memory dynamicParams = DynamicLiquidityModificationParams({ tickLower: -600, tickUpper: 600, liquidityChange: int128(uint128(liquidityToAdd)), snapSqrtPriceX96: 0 }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: liquidityHook, poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 50e18, maxLiquidityAmount1: 110e18, maxLiquidityAmount1: 60e18, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(dynamicParams) }); lbamm.addLiquidity(params, hooksExtraData); ``` ## Example: removeLiquidity ```solidity DynamicLiquidityModificationParams memory p = DynamicLiquidityModificationParams({ tickLower: -600, tickUpper: 600, liquidityChange: -int128(uint128(liquidityToRemove)), snapSqrtPriceX96: 0 // ignored for remove }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: liquidityHook, poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 50e18, maxLiquidityAmount1: 110e18, maxLiquidityAmount1: 60e18, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(dynamicParams) }); amm.removeLiquidity(params, hooksExtraData); ``` --- # Fee Collection and Fee Realization Dynamic Price Pool supports fee collection in two ways: 1. **Explicit fee collection** via `collectFees` 2. **Automatic fee realization** during `addLiquidity` / `removeLiquidity` A key integration difference from “owed-fees then collect later” patterns: - Fees are **not persisted** into a long-lived “tokens owed” storage field that must be collected separately. - Fees are realized and returned as part of these operations, and LBAMM core settles them to the provider in the same call. ## DynamicLiquidityCollectFeesParams ```solidity /** * @dev Parameters for collecting dynamic pool fees. * @param tickLower Lower bound of the liquidity range * @param tickUpper Upper bound of the liquidity range */ struct DynamicLiquidityCollectFeesParams { int24 tickLower; int24 tickUpper; } ``` ## Example: collectFees (no liquidity modification) ```solidity DynamicLiquidityCollectFeesParams memory dynamicParams = DynamicLiquidityCollectFeesParams({ tickLower: -600, tickUpper: 600 }); LiquidityCollectFeesParams memory params = LiquidityCollectFeesParams({ liquidityHook: liquidityHook, poolId: poolId, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(dynamicParams) }); lbamm.collectFees(params, hooksExtraData); ``` --- # Snap-to-Price Behavior (`snapSqrtPriceX96`) When adding liquidity, you may optionally provide a `snapSqrtPriceX96` to **reposition the pool price within an empty region** (a region with zero liquidity). This is intended for cases where the current price is in a gap with no active liquidity and you want to mint liquidity at a specific target price within that same gap. ## Constraints Snapping is only allowed if moving from the current price to the target price: - does **not** cross any tick with liquidity, and - does **not** encounter a tick boundary that would require liquidity transition behavior. Practical mental model: - Snapping can move the pool price within a continuous “zero-liquidity corridor.” - If the move would require crossing into or through liquidity, snapping reverts. ## State updates on snap If snap succeeds, the pool updates only: - `sqrtPriceX96 = snapSqrtPriceX96` - `tick = TickMath.getTickAtSqrtPrice(snapSqrtPriceX96)` There are: - no oracle/observation updates, - no fee growth updates, - no liquidity updates, because the snap is only permitted when no liquidity transitions can occur. --- # Swaps Dynamic Price Pool supports both optimized swap paths: - swap by input amount - swap by output amount Swaps update, in the usual concentrated-liquidity manner: - `sqrtPriceX96` - `tick` - active `liquidity` - fee growth accumulators ## Price limits via `swapExtraData` Swaps may include a price limit encoded in `swapExtraData`. For Dynamic Price Pool: - `swapExtraData` **must** be a 32-byte ABI-encoded `uint160`: - `sqrtPriceLimitX96` ```solidity uint160 sqrtPriceLimitX96 = /* ... */; bytes memory swapExtraData = abi.encode(sqrtPriceLimitX96); ``` If no price limit is desired, integrations may supply an empty bytes value for `swapExtraData` which will set the limit at the minimum or maximum price depending on swap direction. ## Partial fills Dynamic Price Pool supports partial fills when the swap reaches: - the pool’s min/max price boundary, **or** - a swapper-supplied `sqrtPriceLimitX96` In these cases, the pool returns: - the actual amount consumed/produced up to the boundary, and - the swap completes without reverting solely due to hitting the limit. --- # Read Surface: getCurrentPriceX96 Dynamic Price Pool implements the standardized price read surface: ```solidity function getCurrentPriceX96(address amm, bytes32 poolId) external view returns (uint160 sqrtPriceX96); ``` Because state is AMM-partitioned, callers must pass the correct `amm` (the AMM core address whose pool state you want to query). --- # Events and Indexing Dynamic Price Pool emits pool-type-specific events intended for indexers to reconstruct pool and position state. ```solidity /// @dev Event emitted when a swap occurs in a dynamic pool, containing pool-specific details event DynamicPoolSwapDetails( address indexed amm, bytes32 indexed poolId, uint160 sqrtPriceX96, uint128 liquidity, int24 tick ); /// @dev Event emitted when liquidity is added to a dynamic pool position event DynamicPoolLiquidityAdded( address indexed amm, bytes32 indexed poolId, bytes32 indexed positionId, int128 liquidity, int24 tickLower, int24 tickUpper ); /// @dev Event emitted when liquidity is removed from a dynamic pool position event DynamicPoolLiquidityRemoved( address indexed amm, bytes32 indexed poolId, bytes32 indexed positionId, int128 liquidity, int24 tickLower, int24 tickUpper ); ``` ## Indexing guidance (event-driven reconstruction) Indexers should treat `(amm, poolId)` as the pool key. A practical reconstruction strategy: 1. **Track pool state from swaps** - On `DynamicPoolSwapDetails(amm, poolId, sqrtPriceX96, liquidity, tick)`: - set current `sqrtPriceX96`, current `tick`, and current active `liquidity`. 2. **Track position definitions from liquidity events** - On `DynamicPoolLiquidityAdded/Removed`: - map `positionId -> (amm, poolId, tickLower, tickUpper)` - accumulate net liquidity changes per position. 3. **Track fee attribution externally** - Fee growth state is internal to the pool type; the canonical offchain path is: - maintain the pool state from swap events, and - maintain positions from add/remove events, - then compute position fee attribution according to the concentrated-liquidity fee growth model. > The pool also returns realized fees during liquidity operations and explicit fee collection through LBAMM core; those flows can be indexed from the AMM core events associated with those operations. --- # Summary Dynamic Price Pool provides a concentrated-liquidity market design with: - tick-range positions, - moving price during swaps, - liquidity changes specified directly as `liquidityChange` (not token amounts), - fee realization returned during liquidity operations (no persisted “owed-fees” storage), - efficient fee-only collection via `collectFees`, - optional snap-to-price during `addLiquidity` within zero-liquidity regions, and - partial fills when swaps hit min/max boundaries or a user-supplied `sqrtPriceLimitX96`. The most important integration boundary is that pool state is **partitioned by AMM**: always interpret `poolId` in the context of its AMM address. # Fixed Price Pool The Fixed Price Pool enforces a constant token ratio while supporting asymmetric liquidity provisioning through a height-based accounting model. This document explains: 1. How the liquidity model works 2. How pools are created 3. Position lifecycle (add → accrue → collect → withdraw) 4. Swap behavior 5. Quoter usage 6. Event indexing and off-chain height reconstruction This page is written for integrators interacting with **LBAMM Core**. --- # Liquidity Model ## Fixed Ratio Pricing Fixed pools enforce a constant ratio between `token0` and `token1` with a potential range of ratios from `type(uint128).max:1` to `1:type(uint128).max`. The ratio is encoded within fixed pools as: ```solidity packedRatio = (ratio0 << 128) | uint128(ratio1); ``` Example: - ratio0 = 1 - ratio1 = 4 - Swapping 100 token0 yields 400 token1 (before fees). During pool creation a supplied ratio will be simplified by the greatest common divisor between the ratio amounts to avoid duplicate pools with the same parameters and effective price ratio. For example - a ratio of `9:3` will be simplified to `3:1`. The ratio is converted into `sqrtPriceX96` during creation to integrate with LBAMM’s pricing and hook infrastructure. The price does not change after deployment (except for rounding dust). --- ## Height-Based Liquidity Accounting Because price does not move, liquidity attribution is handled through a **height system**. Conceptually: - Liquidity providers deposit token0 and/or token1. - Deposits are represented along height ranges. - Swap flow moves height up or down. - Positions earn fees while current height is within their range. A unit of height corresponds to: > 1 unit of token × number of active liquidity positions at that height. If 6 providers each supply 100 tokens across height 0–100: - Each height unit represents 6 tokens. - A swap of 300 tokens moves height by 50. This model enables: - Fully asymmetric liquidity - Deterministic swap execution - Fair fee attribution --- # Creating a Fixed Pool Pools are created via LBAMM core by calling the `createPool` function with: - `PoolCreationDetails.poolType` set to the Fixed Pool address. - `PoolCreationDetails.poolParams` set to the ABI encoded bytes of a `FixedPoolCreationDetails` struct. --- ## FixedPoolCreationDetails Pool-specific configuration is encoded into `poolParams`. ```solidity struct FixedPoolCreationDetails { uint8 spacing0; uint8 spacing1; uint256 packedRatio; } ``` ### Parameters **spacing0 / spacing1** - Height granularity for token0 and token1. - Liquidity must align to `10^spacing`. - Prevents griefing via excessive micro-positions. - Maximum value: 24. **packedRatio** - Encodes fixed price ratio. - Must represent non-zero ratio. - Simplified internally to smallest values that represent the ratio (ie. 9:3 becomes 3:1). --- ## Encoding Pool Creation Example: ```solidity FixedPoolCreationDetails memory fixedDetails = FixedPoolCreationDetails({ spacing0: 4, spacing1: 4, packedRatio: (uint256(1) << 128) | uint128(4) }); PoolCreationDetails memory details = PoolCreationDetails({ poolType: FIXED_POOL_TYPE, fee: 30, // 0.30% token0: token0, token1: token1, poolHook: address(0), poolParams: abi.encode(fixedDetails) }); (bytes32 poolId,,) = lbamm.createPool( details, "", "", "", "" ); ``` `poolId` is deterministic and encodes: - pool type - fee - height spacing - parameters such as token pair, pool hook, and packed ratio become a hashed representation within `poolId` --- # Position Lifecycle Positions are created by adding liquidity to the pool. Fixed pools utilize the base position identifier supplied by the LBAMM core without modification, meaning that a single liquidity provider has one effective position per liquidity hook that they deposit liquidity with. Additional deposits to a position or partial withdrawals from a position will reposition the net new position - existing position value plus additional deposit (or minus partial withdrawal) - around the current height. Adding and removing liquidity will collect all owed fees for the position. Fees may also be collected through the LBAMM `collectFees` function without modifying liquidity. --- ## Adding Liquidity Liquidity is added to a fixed pool through the `addLiquidity` function on LBAMM core. Liquidity may be provided to either or both tokens in the pool in any amounts that align with the pool's defined token height spacing. Pool-specific parameters are encoded into: ```solidity struct FixedLiquidityModificationParams { uint256 amount0; uint256 amount1; bool addInRange0; bool addInRange1; uint256 endHeightInsertionHint0; uint256 endHeightInsertionHint1; uint256 maxStartHeight0; uint256 maxStartHeight1; } ``` ### Parameter Details **amount0 / amount1** - Maximum deposit amounts. - Actual deposits determined by height alignment. **addInRange0 / addInRange1** - Allows deposit inside current height. - Consumes portion of opposite token to align. - For example: - A pool with a ratio of 1 token0 to 3 token1 has a height spacing of 100 for token0, token0 current height is 10. - If `addInRange0` is false, the liquidity for token0 will deposit at a start height of 100. - If `addInRange0` is true, the liquidity for token0 will deposit at a start height of 0 **and** 30 token1 will be removed from `amount1`. **endHeightInsertionHint0 / endHeightInsertionHint1** - Fixed pools maintain a linked list of heights for each token in the pool to efficiently traverse the heights. - Start heights are simple to insert into the linked list as the nodes above and below the current height are known. - End heights depend on the start height and amount of liquidity being added which could place them anywhere above the current height in the linked list. - Insertion hints optimize the search for the insertion point in the linked list by giving the fixed pool a starting point for the search. - Well defined hints are nodes that are nearby the expected end height of the liquidity position. - Poorly defined hints may result in extra search costs as the pool searches for the correct node to insert at. Integrators should monitor active heights via events to determine optimal end height insertion hints. **maxStartHeight0 / maxStartHeight1** - Front-running protection. - Reverts if computed start height exceeds bound. - Prevents forced out-of-range liquidity. --- ### Example Add Liquidity ```solidity FixedLiquidityModificationParams memory fixedParams = FixedLiquidityModificationParams({ amount0: 100e18, amount1: 0, addInRange0: false, addInRange1: false, endHeightInsertionHint0: knownHeight0, endHeightInsertionHint1: 0, maxStartHeight0: currentHeight0 + 10, maxStartHeight1: 0 }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: liquidityHook, poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 0, maxLiquidityAmount1: 110e18, maxLiquidityAmount1: 0, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(fixedParams) }); lbamm.addLiquidity(params, hooksExtraData); ``` --- ## Collecting Fees Fees may be collected through the LBAMM core `collectFees` function without modifying a liquidity position. Since positions in the fixed pool are determined by the provider and liquidity hook there are no pool parameters required for determining the position and `poolParams` in the `LiquidityCollectFeesParams` struct may be left as empty bytes. Fees accrue while the current height of the token where the provider has supplied liquidity moves between the position's start and end height. Collection: - Updates fee checkpoints - Reduces pool fee balances - Transfers fees to provider --- ### Example Collect Fees ```solidity LiquidityCollectFeesParams memory params = LiquidityCollectFeesParams({ liquidityHook: liquidityHook, poolId: poolId, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: bytes("") }); lbamm.collectFees(params, hooksExtraData); ``` --- ## Removing Liquidity Liquidity is removed from a fixed pool through the `removeLiquidity` function on LBAMM core. Liquidity may be partially or fully removed from a position. When liquidity is to be partially removed the `LiquidityModificationParams.poolParams` value is set to ABI encoded bytes of `FixedLiquidityWithdrawalParams` with `withdrawAll` set to `false` and `params` set to ABI encoded bytes of `FixedLiquidityModificationParams`. The `amount0` and `amount1` fields are the intended withdrawal amounts and the remaining fields are populated in the same manner as adding liquidity. Fixed Pool will compute the value of the position, deduct the intended withdrawal amounts and apply deposit logic to the remaining amounts. If there is any value that cannot be redeposited the withdraw amounts will increase over intended. When liquidity is to be fully removed the `LiquidityModificationParams.poolParams` value is set to ABI encoded bytes of `FixedLiquidityWithdrawalParams` with `withdrawAll` set to `true` and `params` set to ABI encoded bytes of `FixedLiquidityWithdrawAllParams` with `minAmount0` and `minAmount1` providing front-run protection against swaps that change the expected withdrawal amount beyond provider intent without confirmation. ```solidity struct FixedLiquidityWithdrawalParams { bool withdrawAll; bytes params; } ``` --- ### Example Remove Liquidity (Partial) ```solidity FixedLiquidityModificationParams memory fixedParams = FixedLiquidityModificationParams({ amount0: 100e18, amount1: 0, addInRange0: false, addInRange1: false, endHeightInsertionHint0: 20, endHeightInsertionHint1: 0, maxStartHeight0: currentHeight0 + 10, maxStartHeight1: 0 }); FixedLiquidityWithdrawalParams memory withdrawParams = FixedLiquidityWithdrawalParams({ withdrawAll: false, params: abi.encode(fixedParams) }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: liquidityHook, poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 0, maxLiquidityAmount1: 110e18, maxLiquidityAmount1: 0, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(withdrawParams) }); lbamm.removeLiquidity(params, hooksExtraData); ``` --- ### Example Remove Liquidity (Full) ```solidity FixedLiquidityWithdrawAllParams memory fixedParams = FixedLiquidityWithdrawAllParams({ minAmount0: 100e18, minAmount1: 0 }); FixedLiquidityWithdrawalParams memory withdrawParams = FixedLiquidityWithdrawalParams({ withdrawAll: true, params: abi.encode(fixedParams) }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: liquidityHook, poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 0, maxLiquidityAmount1: 110e18, maxLiquidityAmount1: 0, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(withdrawParams) }); lbamm.removeLiquidity(params, hooksExtraData); ``` --- # Swap Behavior Swaps execute at fixed ratio. Height moves instead of price. ## Input-Based Swap - Consumes up to `amountIn` - May partially fill if insufficient output reserves - Returns actual input consumed ## Output-Based Swap - Requests exact output - Input rounded up to meet ratio - May generate dust Example (4:1 pool): - Request 9 token1 - Requires 3 token0 - 3 token0 produces 12 token1 - 3 token1 becomes dust Dust is internally accounted. Swapping near liquidity exhaustion may revert. --- # Quoter Fixed pools expose a companion **FixedPoolQuoter** contract that provides read-only calculations over the current `FixedPoolType` state (position ranges, current heights, fee growth checkpoints, etc.) without requiring the pool type itself to surface every convenience view. The quoter uses a **static delegatecall pattern** to execute logic against the FixedPoolType storage layout while preventing state mutation. This keeps the pool type focused on execution logic while still enabling rich UX and integration tooling. ## Quote the value required for an in-range add When adding liquidity “in range”, the pool may require supplying some amount of the *opposite* token to cover the depth between the current height and the next spacing boundary. This helper returns the approximate amounts needed to perform an in-range add at the pool’s current height. ```solidity (uint256 amount0, uint256 amount1) = fixedPoolQuoter.quoteValueRequiredForInRangeAdd(poolId); ``` - `amount0`: token0 required to add token1 liquidity in-range - `amount1`: token1 required to add token0 liquidity in-range This is primarily a UX / routing primitive. It allows an integrator to: - determine whether the user already holds enough of the opposite token - pre-calculate required swap amounts before liquidity provisioning - preview capital efficiency before submitting a transaction --- ## Quote position principal value and accrued fees This helper computes both principal value and fees for a given position, mirroring the pool type’s internal valuation logic without modifying fee checkpoints. ```solidity (uint256 value0, uint256 value1, uint256 fee0, uint256 fee1) = fixedPoolQuoter.quotePositionValue(poolId, positionId); ``` - `value0`, `value1`: current principal value attributed to the position (token0/token1) - `fee0`, `fee1`: currently accrued, uncollected fees (token0/token1) This is the primary primitive for: - position dashboards and portfolio valuation - estimating withdraw or collect outcomes before submitting a transaction - monitoring liquidity health and behavior near exhaustion boundaries - building analytics around height traversal and fee distribution --- # Events & Indexing Fixed pools emit: ```solidity event FixedPoolPositionUpdated( bytes32 indexed poolId, bytes32 indexed positionId, uint256 startHeight0, uint256 endHeight0, uint256 startHeight1, uint256 endHeight1 ); event FixedPoolHeightConsumed( bytes32 indexed poolId, bool indexed zeroForOne, uint256 currentHeight0, uint256 currentHeight1 ); ``` --- ## Off-Chain Height Reconstruction To maintain height state off-chain: 1. Track `FixedPoolPositionUpdated` 2. Maintain active height intervals 3. Track `FixedPoolHeightConsumed` 4. Update current height 5. Derive in-range liquidity Insertion hints should reference known active heights from this reconstructed state. # Single Provider Pool A **Single Provider Pool** is a pool type where **one address owns and controls all liquidity** for a given `poolId`. This pool type is designed for markets where: - liquidity ownership must be unambiguous (one provider), - liquidity operations must be tightly permissioned (provider-only), - swap pricing is supplied by a hook (policy-defined pricing). A **non-zero pool hook is required**. The hook is responsible for: - providing the execution price on every swap, and - defining (and returning) the allowed liquidity provider for the pool. --- ## Key Guarantees for Integrators You can rely on the following: - **Provider-only liquidity operations:** `addLiquidity`, `removeLiquidity`, and `collectFees` revert unless `msg.sender` is the hook-defined provider for the pool. - **Position identity is trivial:** `positionId = poolId` (no per-provider / per-range positions). - **Reserves are the source of truth:** the pool type uses LBAMM core reserves (`reserve0`, `reserve1`) to determine available liquidity. - **Swap execution price is hook-defined per swap:** the pool type asks the hook for a `sqrtPriceX96` and executes at that price. - **Partial fills are supported:** if the pool cannot satisfy the computed output amount, it caps output to available reserves and adjusts input accordingly. --- ## Pool Creation Parameters Single Provider Pools use the following `poolParams` when calling `createPool` on LBAMM core: ```solidity /** * @dev Parameters for creating a single-provider pool. * * @dev **salt**: Unique salt used to ensure deterministic pool deployment address. * @dev **sqrtPriceRatioX96**: Initial price ratio as sqrt(price) * 2^96 */ struct SingleProviderPoolCreationDetails { bytes32 salt; uint160 sqrtPriceRatioX96; } ``` ### Field semantics - `salt` - Arbitrary `bytes32`. - Included in the `poolId` derivation so multiple pools can exist with the same `(token0, token1, fee, hook)` tuple. - `sqrtPriceRatioX96` - Initial price value stored by the pool type. - Returned by `getCurrentPriceX96(...)` until the first swap occurs. - **Not included in the `poolId` derivation**. ### Pool identity The pool type derives a deterministic `poolId` from the pool type address, fee, `salt`, token pair, and `poolHook`. (The creation-time price is intentionally excluded.) --- ## Pool Hook Requirements Single Provider Pools **must** be created with a non-zero `poolHook`. A zero address hook causes pool creation to revert. The hook must implement `ISingleProviderPoolHook`: ```solidity // SPDX-License-Identifier: MIT pragma solidity 0.8.24; import "@limitbreak/lb-amm-core/src/interfaces/hooks/ILimitBreakAMMPoolHook.sol"; /** * @notice Interface definition for a single provider pool's hook to get price and provider. */ interface ISingleProviderPoolHook is ILimitBreakAMMPoolHook { struct HookPoolPriceParams { bool inputSwap; bytes32 poolId; address tokenIn; address tokenOut; uint256 amount; } /** * @notice Executed during a swap to return the price to execute at. */ function getPoolPriceForSwap( SwapContext calldata context, HookPoolPriceParams calldata poolPriceParams, bytes calldata hookData ) external returns (uint160 poolPrice); /** * @notice Returns the single allowed liquidity provider for the pool. */ function getPoolLiquidityProvider( bytes32 poolId ) external view returns (address provider); } ``` ### What integrators should assume about the provider - The **allowed provider** is defined by the hook and is pool-specific. - LBAMM core uses `msg.sender` as the liquidity operation “provider” identity for these checks. - If you insert an external router, the router becomes the provider for hook purposes (and will fail unless it is the allowed provider). > How the hook *decides* or *initializes* the provider is intentionally not specified by the pool type. It may be set during pool creation via hook data, derived from internal state, or computed by custom logic. --- ## Liquidity Modification Parameters Single Provider Pools use the following `poolParams` when calling `addLiquidity` and `removeLiquidity` on LBAMM core: ```solidity /** * @notice Parameters for modifying single-provider pool liquidity. * @dev Used when adding or removing liquidity from single-provider pools. * * @dev **amount0**: Exact amount of token0 to add or remove. * @dev **amount1**: Exact amount of token1 to add or remove. */ struct SingleProviderLiquidityModificationParams { uint256 amount0; uint256 amount1; } ``` ### Field semantics - `amount0`, `amount1` are **exact** amounts. - For `addLiquidity`: exact deposits into the pool. - For `removeLiquidity`: exact withdrawals from the pool. - Either side may be set to zero (single-sided add/remove is allowed), subject to: - token hooks, - the pool hook, - and available reserves for withdrawals. --- ## Reserves and Fees Single Provider Pools do not maintain separate pool-type liquidity state. They use LBAMM core `PoolState` for: - `reserve0`, `reserve1` (swapable reserves) - `feeBalance0`, `feeBalance1` (accrued LP fees) All LP fees accrue to the single provider. If the pool is configured with the dynamic fee sentinel at creation time, the pool hook may provide dynamic LP fees. Otherwise, the pool uses a fixed LP fee. --- ## Swap Pricing On each swap, the pool calls the pool hook's `getPoolPriceForSwap` function to determine the price to execute the swap at. ### Fixed-Price Math Used Per Swap Single Provider Pools execute swaps at a hook-supplied `sqrtPriceX96`, defined as: - `sqrt(token1 / token0) * 2^96` This value fully determines the execution price for the swap. --- #### Input-Based Swaps For input-based swaps: - The trader specifies `amountIn`. - The pool computes `amountOut` using the hook-supplied price. - Output amounts are rounded **down**. If the output amount exceeds the pool's reserve of the output token, the pool will attempt to fill the swap as an output-based swap with the reserve amount as the output. --- #### Output-Based Swaps For output-based swaps: - The trader specifies `amountOut`. - The pool computes the required `amountIn`. - Input amounts are rounded **up**. Rounding up ensures the pool never undercharges input. --- ## Current Price Read Surface Single Provider Pools expose a “last execution price” read surface: ```solidity getCurrentPriceX96(address amm, bytes32 poolId) ``` Semantics: - Before any swap occurs: returns the creation-time `sqrtPriceRatioX96`. - After swaps: returns the exact `sqrtPriceX96` returned by the hook for the most recent swap. --- ## Swap Event Single Provider Pools emit an additional swap details event: ```solidity /// @dev Event emitted when a swap is executed in a single-provider pool. event SingleProviderPoolSwapDetails( bytes32 indexed poolId, uint256 sqrtPriceX96, uint256 liquidity ); ``` - `sqrtPriceX96` is the execution price used for the swap. - `liquidity` is the output reserve **before** the swap (as currently defined). Indexers can treat LBAMM core reserve and fee balance updates as authoritative state. --- ## Examples The following examples show typical construction of `poolParams` for this pool type. These snippets intentionally omit unrelated fields (token approvals, hookData layout, and token hook configuration). ### Create a Single Provider Pool ```solidity SingleProviderPoolCreationDetails memory sp = SingleProviderPoolCreationDetails({ salt: bytes32(uint256(1)), sqrtPriceRatioX96: initialSqrtPriceX96 }); bytes memory poolParams = abi.encode(sp); amm.createPool( PoolCreationDetails({ poolType: singleProviderPoolType, fee: feeBps, token0: token0, token1: token1, poolHook: poolHook, // MUST be non-zero poolParams: poolParams, }), token0HookData, // if needed by token0 hook token1HookData, // if needed by token1 hook poolHookData, // hook-defined (may initialize provider) liquidityData // optional initial deposit ); ``` ### Add Liquidity (provider-only) ```solidity SingleProviderLiquidityModificationParams memory singleParams = SingleProviderLiquidityModificationParams({ amount0: 100e18, amount1: 0 }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: address(0), // position hooks not used; pool hook enforces constraints poolId: poolId, minLiquidityAmount0: 100e18, minLiquidityAmount1: 0, maxLiquidityAmount1: 100e18, maxLiquidityAmount1: 0, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(singleParams) }); lbamm.addLiquidity(params, hooksExtraData); ``` ### Remove Liquidity (provider-only) ```solidity SingleProviderLiquidityModificationParams memory singleParams = SingleProviderLiquidityModificationParams({ amount0: 90e18, amount1: 10e18 }); LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: address(0), // position hooks not used; pool hook enforces constraints poolId: poolId, minLiquidityAmount0: 85e18, minLiquidityAmount1: 5e18, maxLiquidityAmount1: 95e18, maxLiquidityAmount1: 15e18, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: abi.encode(singleParams) }); lbamm.addLiquidity(params, hooksExtraData); amm.removeLiquidity(params, hooksExtraData); ``` ### Collect Fees (provider-only) ```solidity LiquidityCollectFeesParams memory params = LiquidityCollectFeesParams({ liquidityHook: address(0), // position hooks not used; pool hook enforces constraints poolId: poolId, maxHookFee0: 1e17, maxHookFee1: 0, poolParams: bytes("") }); lbamm.collectFees(params, hooksExtraData); ``` --- ## Summary A Single Provider Pool has: - provider-only liquidity operations enforced via the pool type with the pool hook supplying the allowed provider address, - hook-defined per-swap pricing (`sqrtPriceX96`), - core-reserve-based liquidity availability, and - partial fill behavior when reserves are insufficient The pool hook is the policy surface: it defines who controls liquidity and what price swaps execute at. # Adding & Removing Liquidity This page is the **canonical integrator reference** for LBAMM liquidity operations: - `addLiquidity` - `removeLiquidity` - `collectFees` - `collectTokensOwed` (settling “owed” amounts / transfer debt) It is **pool-type-agnostic**: all pool types share the same core flow, while **`poolParams`** is interpreted by the pool type contract. --- ## Mental model A liquidity operation is: 1. **Pool-type computation** (how much liquidity changes + what fees are collected) 2. **Hook validation + hook fees** (token0/token1/position/pool) 3. **Core accounting updates** (reserves / fee balances) 4. **Net settlement** (collect from provider or pay to provider) 5. **(Optional) queued hook-fee transfers** executed after the operation Two important consequences for integrators: - **Hook fees change what the provider pays/receives.** You must bound them. - **Outbound transfers can become “owed” instead of reverting.** Providers may need to claim later. --- ## Core structs ### Hook calldata bundle ### Modify liquidity params ```solidity struct LiquidityModificationParams { address liquidityHook; bytes32 poolId; // Bounds on the liquidity-change amounts (exclusive of hook fees). uint256 minLiquidityAmount0; uint256 minLiquidityAmount1; uint256 maxLiquidityAmount0; uint256 maxLiquidityAmount1; // Bounds on hook fees (token-denominated). uint256 maxHookFee0; uint256 maxHookFee1; // Pool-type-specific bytes. bytes poolParams; } ``` **Key semantics (integrator-critical):** - `minLiquidityAmount*` / `maxLiquidityAmount*` are checked against the **raw pool-type amounts** (`deposit*` / `withdraw*`) **exclusive of hook fees**. - Hook fees are bounded separately by `maxHookFee0` / `maxHookFee1`. - `liquidityHook` is part of position identity (see below). --- ### Collect fees params ```solidity struct LiquidityCollectFeesParams { address liquidityHook; bytes32 poolId; uint256 maxHookFee0; uint256 maxHookFee1; bytes poolParams; } ``` --- Liquidity operations include per-hook calldata: ```solidity struct LiquidityHooksExtraData { bytes token0Hook; bytes token1Hook; bytes liquidityHook; bytes poolHook; } ``` > These fields are opaque to LBAMM core. They are forwarded to the configured hooks (if any). --- ### LiquidityContext (what hooks see) ```solidity struct LiquidityContext { address provider; address token0; address token1; bytes32 positionId; } ``` --- ## Position identity and `liquidityHook` binding LBAMM derives a base position identity that includes: - `provider` (the caller for these functions) - `liquidityHook` (position hook address) - `poolId` If you call `removeLiquidity` / `collectFees` with a different `liquidityHook` than the one used when adding liquidity, you will not resolve the same position. **Integrator rule:** > Treat `liquidityHook` as part of the position key. Persist it and reuse it for future operations. --- ## API surface ### addLiquidity ```solidity function addLiquidity( LiquidityModificationParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external payable returns ( uint256 deposit0, uint256 deposit1, uint256 fees0, uint256 fees1 ); ``` **Returns:** - `deposit0`, `deposit1`: amounts deposited into pool reserves - `fees0`, `fees1`: fee amounts collected from pool fee balances during this operation --- ### removeLiquidity ```solidity function removeLiquidity( LiquidityModificationParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external returns ( uint256 withdraw0, uint256 withdraw1, uint256 fees0, uint256 fees1 ); ``` **Returns:** - `withdraw0`, `withdraw1`: amounts withdrawn from pool reserves - `fees0`, `fees1`: fee amounts collected from pool fee balances during this operation --- ### collectFees ```solidity function collectFees( LiquidityCollectFeesParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external returns (uint256 fees0, uint256 fees1); ``` **Returns:** - `fees0`, `fees1`: fee amounts collected from pool fee balances and distributed (or recorded as owed) --- ## Hook participation and hook fees Liquidity operations may involve up to four hook calls: - token0 hook - token1 hook - position liquidity hook (`liquidityHook`) - pool hook A hook may return `hookFee0` and/or `hookFee1`. **Integrator-impact:** - On **addLiquidity**, hook fees can **increase** the net tokens collected from the provider. - On **removeLiquidity** and **collectFees**, hook fees can **reduce** the net tokens paid out to the provider (or increase what must be returned). LBAMM enforces fee bounds: ```solidity if (hookFee0 > liquidityParams.maxHookFee0 || hookFee1 > liquidityParams.maxHookFee1) { revert LBAMM__ExcessiveHookFees(); } ``` **Integrator rule:** > Always set `maxHookFee0/maxHookFee1` to values you are willing to pay. Assume hook fees may be nonzero. --- ## Step-by-step execution semantics This section describes what LBAMM core does, independent of pool type. ### addLiquidity flow (pool-type agnostic) 1. Validate pool exists. 2. Call pool type: - returns `(positionId, deposit0, deposit1, fees0, fees1)` 3. Enforce amount bounds (**exclusive of hook fees**): - `deposit*` must be within `[minLiquidityAmount*, maxLiquidityAmount*]` 4. Execute liquidity hooks (token0/token1/position/pool) and obtain `hookFee0/hookFee1` 5. Enforce hook fee bounds (`maxHookFee*`) 6. Update pool state: - reserves increase by `deposit*` - fee balances decrease by `fees*` 7. Settle net amounts with provider: ```solidity netAmount0 = deposit0 - fees0 + hookFee0 netAmount1 = deposit1 - fees1 + hookFee1 ``` 8. Emit `LiquidityAdded(poolId, provider, deposit0, deposit1, fees0, fees1)` 9. Execute any queued “hook fees by hook” transfers (if hooks requested them) --- ### removeLiquidity flow (pool-type agnostic) 1. Validate pool exists. 2. Call pool type: - returns `(positionId, withdraw0, withdraw1, fees0, fees1)` 3. Enforce amount bounds (**exclusive of hook fees**): - `withdraw*` must be within `[minLiquidityAmount*, maxLiquidityAmount*]` 4. Execute liquidity hooks and obtain `hookFee0/hookFee1` 5. Enforce hook fee bounds (`maxHookFee*`) 6. Update pool state: - reserves decrease by `withdraw*` - fee balances decrease by `fees*` 7. Settle net amounts with provider: ```solidity netAmount0 = -(withdraw0 + fees0) + hookFee0 netAmount1 = -(withdraw1 + fees1) + hookFee1 ``` 8. Emit `LiquidityRemoved(poolId, provider, withdraw0, withdraw1, fees0, fees1)` 9. Execute queued hook-fee transfers (if any) > Note the sign convention: negative net amounts indicate **payout to provider**. --- ### collectFees flow (pool-type agnostic) 1. Validate pool exists. 2. Call pool type: - returns `(positionId, fees0, fees1)` 3. Execute liquidity hooks and obtain `hookFee0/hookFee1` 4. Enforce hook fee bounds (`maxHookFee*`) 5. Update pool state: - fee balances decrease by `fees*` 6. Settle net amounts with provider: ```solidity netAmount0 = -(fees0) + hookFee0 netAmount1 = -(fees1) + hookFee1 ``` 7. Emit `FeesCollected(poolId, provider, fees0, fees1)` 8. Execute queued hook-fee transfers (if any) --- ## Net settlement, native value, and “owed tokens” (debt) LBAMM settles liquidity operations via an internal helper that uses a clear sign convention: - `netAmount > 0` → **collect tokens from provider** - `netAmount < 0` → **distribute tokens to provider** ### Wrapped native support (`msg.value`) If `msg.value > 0` and the relevant token is `wrappedNative`, the AMM can deposit wrapped native on behalf of the provider. If native value is sent but not used: ```solidity revert LBAMM__ValueNotUsed(); ``` ### Failed outbound transfers become “owed” When distributing **ERC-20** tokens to the provider (i.e., `netAmount < 0` and token is not wrapped native), LBAMM attempts a transfer: - If the transfer fails, LBAMM stores the amount as **tokens owed** instead of reverting. This means integrators must handle a real possibility: > A successful liquidity operation may not immediately pay out all tokens. Some amounts may be claimable later. ### Claiming owed tokens Providers can claim owed balances: ```solidity function collectTokensOwed(address[] calldata tokensOwed) external; ``` Successful claims emit: ```solidity event TokensClaimed(address owedTo, address tokenOwed, uint256 amount); ``` If an owed transfer fails during claim, the claim reverts: ```solidity revert LBAMM__TokenOwedTransferFailed(); ``` --- ## Events Core emits: ```solidity event LiquidityAdded(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1); event LiquidityRemoved(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1); event FeesCollected(bytes32 indexed poolId, address indexed provider, uint256 fees0, uint256 fees1); ``` Pool types may emit additional pool-specific events. --- ## Integration snippets These are **generic** examples. Replace `poolParams` with pool-type-specific bytes. ### Add liquidity (minimal, pool-type-agnostic) ```solidity LiquidityModificationParams memory p = LiquidityModificationParams({ liquidityHook: positionHook, // address(0) if none poolId: poolId, // Bound the raw deposit amounts returned by the pool type: minLiquidityAmount0: min0, minLiquidityAmount1: min1, maxLiquidityAmount0: max0, maxLiquidityAmount1: max1, // Bound hook fees you are willing to pay: maxHookFee0: maxHookFee0, maxHookFee1: maxHookFee1, // Pool-type-defined: poolParams: poolParams }); LiquidityHooksExtraData memory hx = LiquidityHooksExtraData({ token0Hook: token0HookData, token1Hook: token1HookData, liquidityHook: positionHookData, poolHook: poolHookData }); // Approvals (if token is not wrapped native) IERC20(token0).approve(address(amm), expectedUpperBound0); IERC20(token1).approve(address(amm), expectedUpperBound1); // If token0 or token1 is wrappedNative, you may supply msg.value as needed. (uint256 deposit0, uint256 deposit1, uint256 fees0, uint256 fees1) = amm.addLiquidity{value: msgValue}(p, hx); ``` **Integrator notes:** - Persist `liquidityHook` you used. It is required to resolve the same position later. - If token transfers to the provider fail (not on add, but on remove/collect), the provider may need to call `collectTokensOwed`. --- ### Remove liquidity + collect fees ```solidity // IMPORTANT: liquidityHook must match the position’s identity. LiquidityModificationParams memory rp = LiquidityModificationParams({ liquidityHook: positionHook, poolId: poolId, // Bound the raw withdraw amounts returned by the pool type: minLiquidityAmount0: minOut0, minLiquidityAmount1: minOut1, maxLiquidityAmount0: maxOut0, maxLiquidityAmount1: maxOut1, // Bound hook fees charged during removal: maxHookFee0: maxHookFee0, maxHookFee1: maxHookFee1, poolParams: poolParams }); (uint256 w0, uint256 w1, uint256 f0, uint256 f1) = amm.removeLiquidity(rp, hx); // Collect fees (may be separate or periodic) LiquidityCollectFeesParams memory cp = LiquidityCollectFeesParams({ liquidityHook: positionHook, poolId: poolId, maxHookFee0: maxHookFee0, maxHookFee1: maxHookFee1, poolParams: poolParams }); (uint256 cf0, uint256 cf1) = amm.collectFees(cp, hx); // If any outbound transfers failed and were recorded as owed: address; owed[0] = token0; owed[1] = token1; amm.collectTokensOwed(owed); ``` --- ## Common failure modes (what can revert and why) Even before the protocol is live, integrators should assume these classes of failures: - **Pool existence**: pool not created / invalid `poolId` - **Pool type call reverts**: invalid `poolParams`, pool-type-specific invariants fail - **Bounds violated**: - raw `deposit*/withdraw*` outside `[minLiquidityAmount*, maxLiquidityAmount*]` - hook fees exceed `maxHookFee*` → `LBAMM__ExcessiveHookFees()` - **Hook reverts**: token/position/pool hook validation fails - **Native value misuse**: `msg.value > 0` but wrapped native not used → `LBAMM__ValueNotUsed()` - **State accounting issues**: reserve/fee balance underflow/overflow checks; debt storage overflow guards - **Claiming owed tokens**: owed transfer fails on claim → `LBAMM__TokenOwedTransferFailed()` --- ## Key takeaways - Liquidity ops are **pool-type-defined**, but **settlement and enforcement are standardized**. - Hook fees can materially change provider economics. Always bound with `maxHookFee0/maxHookFee1`. - Some outbound payments may become **tokens owed** rather than revert; providers may need to call `collectTokensOwed`. - `liquidityHook` is part of the position identity — persist and reuse it. # Liquidity Positions This page defines what a **position** is in LBAMM, how positions are identified, and what guarantees exist at the protocol level. Crucially, this page describes the **abstract position model** shared across all pool types. The concrete meaning of a position—ranges, pricing, ownership semantics, or accounting—is defined entirely by the pool type. --- ## What is a position? A **position** represents a share of a pool’s liquidity reserves and accrued fees. * Every position is associated with **exactly one pool** * A position’s economic meaning is defined by the **pool type’s liquidity model** * The LBAMM core does **not** understand how positions are represented within a pool type From the core AMM’s perspective, a position is simply an identifier that a pool type uses to attribute liquidity changes and fee collection. > **Important:** Positions are *not* a protocol-level primitive with universal semantics. They are defined and interpreted by pool implementations. --- ## Position identity ### Base position identifier When liquidity is added, removed, or fees are collected, the core AMM derives a **base position identifier** deterministically. The base position ID is computed from: * The liquidity provider address * The liquidity hook address * The target `poolId` ```solidity bytes32 basePositionId = keccak256( abi.encode( provider, liquidityHook, poolId ) ); ``` Because the `poolId` is included, base position identifiers: * Are **unique per pool** * Cannot be reused across different pools ### Pool-type position identifiers Pool types may use the base position ID directly, or derive a **final position ID** by combining it with pool-type-specific parameters. For example, a pool type may: * Use the base position ID as-is * Hash the base ID together with encoded position parameters The only protocol-level expectation is that a pool type uses a **stable and deterministic method** to resolve the same position across: * Liquidity additions * Liquidity removals * Fee collection --- ## Position lifecycle At the core AMM level, positions do not have an explicit lifecycle beyond **liquidity modification**. The core exposes three position-related actions: * Add liquidity * Remove liquidity * Collect fees Whether these actions: * Create a new position * Modify an existing position * Represent directional changes on the same position is entirely up to the pool type’s model. --- ## What the core AMM does not know The core AMM does **not** understand or enforce: * Position ranges or parameters * Position ownership or transferability rules * How liquidity is split across positions * How fees are allocated between positions * Whether positions are enumerable or transferable Any such behavior must be implemented by the pool type or enforced via hooks. --- ## Hooks and positions During liquidity operations, hooks may observe position-related context. Specifically, position hooks receive a `LiquidityContext` that includes: * The liquidity provider address * The pool’s token pair * The resolved position identifier This allows hooks to: * Validate whether a provider may modify a position * Enforce custom permissioning or constraints All position-level authorization is expected to be enforced via hooks. The core AMM does not impose position permissions. --- ## Fees and positions LP fees are attributed to positions **by the pool type**. * The core AMM tracks only pool-level fee balances * Pool types determine how fees accrue to individual positions The core never allocates fees directly to positions. --- ## Common misconceptions The following statements are **false** in LBAMM: * “Positions are first-class objects in LBAMM” * “Positions are portable across pool types” * “Positions are NFTs” Positions exist only within the context of a specific pool type’s implementation. --- ## Why this matters By keeping positions pool-defined rather than protocol-defined, LBAMM enables: * Diverse liquidity models without shared constraints * Clear trust boundaries between core and pool logic * Explicit, hook-driven permissioning Subsequent sections describe how individual pool types implement and interpret positions in detail. # Position Policy Enforcement LBAMM externalizes liquidity policy to hooks and pool types. Core provides deterministic execution, identity guarantees, and bounded fee surfaces. Hooks and pool types provide compliance logic and participation rules. This page explains how compliance and policy compose across: - Token hooks - Position hooks - Pool hooks For hook interfaces and execution order, see: - `hooks/hook-types/position-hooks` - `liquidity/adding-removing-liquidity` --- ## Core Guarantees LBAMM core guarantees: - `provider = msg.sender` for liquidity operations - Deterministic hook ordering (token → position → pool) - Hook fee bounding via `maxHookFee0` / `maxHookFee1` - Atomic execution (hook revert = full revert) - Deferred payout support via `tokensOwed` when transfers fail Core does not define compliance rules. It provides the execution framework within which policy is enforced. --- ## Policy Surfaces and Responsibilities ### Token Hooks — Token-Global Compliance Token hooks are the canonical surface for: - Token-wide compliance guarantees - Eligibility validation - Required authorization flows - Mandatory integration constraints If a token requires compliance, it should enforce that policy in its token hook so that guarantees remain consistent across all pools and venues. Token hooks may also require specific liquidity hooks to be used when creating positions. This requirement can be dynamic. For example, a token hook may enforce different `liquidityHook` addresses depending on the provider creating the position. This enables: - Different compliance programs for different participant classes - Distinct policy modules for institutional vs. retail providers - Position-scoped policy variation without altering pool semantics In this model, the token defines which position-level policy must be bound to each provider. --- ### Position Hooks — Position-Scoped Compliance Position hooks bind policy directly to a specific liquidity position identity: - Program- or tranche-scoped requirements - Time-based enablement (e.g., vesting schedules) - Operator- or custodian-scoped workflows - Position-specific fee logic Because `liquidityHook` is part of the position identity: - Policy becomes cryptographically bound to that position. - Supplying a different hook resolves a different position. Position hooks allow compliant positions to coexist with unrestricted positions for the same token. --- ### Pool Hooks — Pool-Level Compliance Isolation Pool hooks provide pool-wide policy surfaces. They are well suited for: - Venue-scoped participation requirements - Compliance isolation for LPs - Pools integrating tokens without token hooks - Application-specific integration guarantees Pool hooks enable compliant liquidity environments without requiring token-level changes. --- ## Composability Model A liquidity operation may invoke: - token0 hook - token1 hook - position hook - pool hook Each layer may: - Validate eligibility - Enforce policy - Return hook fees Hook fees are bounded by the caller via `maxHookFee0` and `maxHookFee1`. If bounds are exceeded, the operation reverts. This preserves predictable economics for liquidity providers. --- ## Settlement Considerations Policy validation and settlement are distinct phases. If outbound transfers fail during `removeLiquidity` or `collectFees`, the amount is recorded as `tokensOwed` rather than reverting. This ensures: - Policy enforcement remains atomic - Settlement remains reliable - Providers can claim owed balances later Compliance guarantees are preserved even if payout is deferred. --- ## Design Guidance for Integrators When designing compliant liquidity systems: - Enforce token-global requirements in token hooks. - Bind program- or position-specific logic in position hooks. - Use pool hooks for venue-scoped compliance isolation. - Persist and reuse `liquidityHook` for each position. - Bound hook fees. - Handle potential `tokensOwed` claims. Policy in LBAMM is explicit, composable, and opt-in through hooks. --- ## Summary LBAMM separates execution from policy. Core provides: - Identity stability - Deterministic ordering - Bounded fee surfaces - Reliable settlement semantics Hooks and pool types provide: - Compliance guarantees - Eligibility validation - Programmatic participation rules - Isolation where needed Liquidity policy is enabled — not imposed — through composable hook architecture. # Direct Swaps A **direct swap** is a taker order in which the taker matches against a **maker order represented through a transfer handler** with the direct swap executor providing the output tokens for the maker's order and receiving the maker's input tokens in exchange. This makes `directSwap` the primitive for building fillable order systems such as signed permits or onchain order books. --- # Function Signature ```solidity function directSwap( SwapOrder calldata swapOrder, DirectSwapParams calldata directSwapParams, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData calldata swapHooksExtraData, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` --- # SwapOrder Direct swaps use the same `SwapOrder` struct as pool swaps. ```solidity struct SwapOrder { uint256 deadline; address recipient; int256 amountSpecified; // >0 = swap by input, <0 = swap by output uint256 minAmountSpecified; // no partial fills in directSwap uint256 limitAmount; address tokenIn; address tokenOut; } ``` ## Swap Mode Convention - `amountSpecified > 0` → swap by input - `amountSpecified < 0` → swap by output The sign convention is identical to pool swaps for consistency of intent. --- # DirectSwapParams ```solidity struct DirectSwapParams { uint256 swapAmount; uint256 maxAmountOut; uint256 minAmountIn; } ``` These parameters bound execution when hook fees are applied: - `swapAmount` → base amount for direct execution - `maxAmountOut` → maximum the executor may be required to provide - `minAmountIn` → minimum the executor must receive Direct swaps do not partially fill. If bounds are violated, the transaction reverts. --- # Fee Structs ```solidity struct BPSFeeWithRecipient { uint16 BPS; address recipient; } struct FlatFeeWithRecipient { uint256 amount; address recipient; } ``` Both exchange fees and flat fees are supported. --- # SwapHooksExtraData Only token hook data is supported. ```solidity struct SwapHooksExtraData { bytes tokenInHook; bytes tokenOutHook; bytes poolHook; // must be empty bytes poolType; // must be empty } ``` If `poolHook` or `poolType` are non-empty, the transaction reverts. --- # transferData (Transfer Handler Settlement) `directSwap` is typically used with a **transfer handler**. If `transferData.length >= 32`: - First 32 bytes: ABI-encoded `address transferHandler` - Remaining bytes: handler-specific `transferExtraData` Conceptually: ```text transferData = [32 bytes: abi.encode(transferHandler)] [N bytes : transferExtraData] ``` --- # Mental Model A direct swap is: - A taker order submitted to LBAMM - Matched directly against a maker order represented by a transfer handler Example: - Maker signs a permit offering Token A for Token B - Taker calls `directSwap` - LBAMM collects Token B from the taker - PermitTransferHandler validates and consumes the permit, supplying Token A to LBAMM - LBAMM disburses Token A to the taker and Token B to the maker This enables offchain order books of signed maker intent. --- # Solidity Example (Permit-Based Direct Swap) ```solidity SwapOrder memory order = SwapOrder({ deadline: block.timestamp + 300, recipient: msg.sender, amountSpecified: int256(1e18), minAmountSpecified: 0, limitAmount: 0, tokenIn: address(tokenA), tokenOut: address(tokenB) }); DirectSwapParams memory params = DirectSwapParams({ swapAmount: 1e18, maxAmountOut: 1.02e18, minAmountIn: 0.98e18 }); // PermitTransferHandler encoding bytes1 permitType = 0x00; // FILL_OR_KILL_PERMIT bytes memory permitStruct = abi.encode(/* permit fields */); bytes memory transferData = abi.encodePacked( abi.encode(address(PERMIT_TRANSFER_HANDLER)), permitType, permitStruct ); amm.directSwap( order, params, BPSFeeWithRecipient({BPS: 0, recipient: address(0)}), FlatFeeWithRecipient({amount: 0, recipient: address(0)}), SwapHooksExtraData("", "", "", ""), transferData ); ``` --- # viem Example ```ts import { encodeAbiParameters, concatHex } from 'viem' const transferData = concatHex([ encodeAbiParameters([{ type: 'address' }], [permitHandler]), '0x00', // FILL_OR_KILL_PERMIT permitStructEncoded ]) await walletClient.writeContract({ address: ammAddress, abi: ammAbi, functionName: 'directSwap', args: [ swapOrder, directSwapParams, { BPS: 0, recipient: zeroAddress }, { amount: 0n, recipient: zeroAddress }, { tokenInHook: '0x', tokenOutHook: '0x', poolHook: '0x', poolType: '0x' }, transferData, ], }) ``` # Executors & Solvers This page defines the roles of **executors**, **makers**, and **solvers** in LBAMM order flow. The key invariant to remember: > The **executor is always `msg.sender` of the AMM call**. > All execution semantics — including fee collection and hook visibility — are defined relative to that address. --- ## Core Roles ### Executor The **executor** is the address that calls the AMM contract. In all swap functions: ```solidity function singleSwap(...) external payable returns (...); function multiSwap(...) external payable returns (...); function directSwap(...) external payable returns (...); ``` The executor is strictly: ```solidity msg.sender ``` The executor: - Supplies input tokens - Pays exchange fees and fee-on-top - Triggers all hook execution - Is visible to token, pool, and handler logic The protocol does not track `tx.origin` or any “true initiator.” For all protocol-level reasoning, **executor = caller**. --- ### Maker The **maker** is the party whose intent is being executed. Examples: - A user who signs a permit authorizing the transfer of tokens - A user who places an onchain CLOB order - Any actor whose order defines swap parameters but does not call the AMM directly The maker does **not** need to be the executor. In many advanced flows, the maker and executor are different parties. --- ### Taker The **taker** is the counterparty that consumes maker liquidity. - In a `directSwap`, the executor is the taker. - In a pool-based swap filling a maker order, the executor may be: - the taker directly, or - a **solver/searcher** acting on behalf of the taker. --- ### Solver (Searcher) A **solver** is a professional executor that: - Observes maker orders (permits, CLOB orders, etc.) - Searches for profitable execution paths offchain - Submits a transaction that matches maker intent - Captures surplus via fee surfaces such as `feeOnTop` The searching process is strictly offchain. From the protocol’s perspective, a solver is simply an executor. --- ## Execution Context ### SwapOrder All pool-based swaps use: ```solidity struct SwapOrder { uint256 deadline; address recipient; int256 amountSpecified; uint256 minAmountSpecified; uint256 limitAmount; address tokenIn; address tokenOut; } ``` --- ## Fee Configuration Two fee surfaces are available at the swap level. ### Exchange Fee (BPS) ```solidity struct BPSFeeWithRecipient { address recipient; uint256 BPS; } ``` **Invalid when:** - `BPS > 10_000` - `BPS > 0 && recipient == address(0)` **Semantics:** - Applied to the full input amount required from the executor - Scales down proportionally on partial fill - Charged once per `singleSwap` or `multiSwap` - Not charged per hop in multi-hop execution --- ### Fee On Top (Flat) ```solidity struct FlatFeeWithRecipient { address recipient; uint256 amount; } ``` **Invalid when:** - `amount > 0 && recipient == address(0)` **Semantics:** - Flat fee taken from executor - Does **not** scale down on partial fill - Charged once per execution - Not charged per hop --- ## multiSwap Fee Behavior In `multiSwap`: - Exchange fee and fee-on-top are applied **once** to the total input - Intermediate hops: - Do **not** pay exchange fee or fee-on-top - May still incur token hook fees per hop --- ## Direct Swaps ```solidity function directSwap( SwapOrder calldata swapOrder, DirectSwapParams calldata directSwapParams, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData calldata swapHooksExtraData, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` In a direct swap: - The executor supplies output tokens directly - No AMM pool liquidity is used - Pool hooks are not invoked - Token hooks are invoked The executor is the taker. --- ## Solvers Filling Maker Intent via Pools A common advanced flow: 1. Maker signs a permit: - Sell 100 TokenA - Receive 10 TokenB 2. A pool exists where: - 90 TokenA → 10 TokenB 3. A solver executes `singleSwap` and captures surplus. ### Example Flow **Maker intent:** - 100 TokenA → 10 TokenB **Pool pricing:** - 90 TokenA → 10 TokenB **Solver execution:** - `swapOrder.amountSpecified = 100 TokenA` - `feeOnTop.amount = 10 TokenA` - `feeOnTop.recipient = solver` - `recipient = maker` ### Execution Outcome - 100 TokenA collected from executor - 10 TokenA paid to solver as fee-on-top - 90 TokenA routed through pool - Pool returns 10 TokenB - 10 TokenB sent to maker Result: - Maker receives exactly 10 TokenB for 100 TokenA - Solver captures 10 TokenA - Pool executes swap for 90 TokenA --- ### Pseudocode (Solver Perspective) ```solidity SwapOrder memory order = SwapOrder({ deadline: block.timestamp + 300, recipient: maker, amountSpecified: 100e18, // 100 TokenA minAmountSpecified: 100e18, limitAmount: 10e18, // must deliver 10 TokenB tokenIn: TokenA, tokenOut: TokenB }); FlatFeeWithRecipient memory feeOnTop = FlatFeeWithRecipient({ recipient: solver, amount: 10e18 // captured surplus }); BPSFeeWithRecipient memory exchangeFee = BPSFeeWithRecipient({ recipient: address(0), BPS: 0 }); amm.singleSwap( order, poolId, exchangeFee, feeOnTop, swapHooksExtraData, permitTransferData ); ``` --- ## Partial Fills If partial fills occur: - Exchange fee scales proportionally - Fee-on-top does **not** scale down Executors should ensure their fee configuration remains acceptable given the possibility of partial execution. --- ## Executor Visibility & Hook Interaction The executor is always `msg.sender`. Hooks may inspect and restrict based on: - Executor address - Recipient address - Executor → recipient relationships - Transfer handler usage Some tokens may permit only specific executors. If an integrator inserts a router or aggregator: - That router becomes the executor - Hooks see the router, not the end-user Integrators must design flows accordingly. --- ## Summary - Executor = `msg.sender` - Maker = party whose intent is being executed - Taker = counterparty consuming maker liquidity - Solver = professional executor that searches offchain and captures surplus - Exchange fees scale with fill - Fee-on-top does not - multiSwap charges fees once - Hooks see the executor directly # EIP-7702 Delegation & LBAMM Compliance EIP-7702 enables persistent delegation of an account’s execution authority to a contract. When an account delegates under 7702: - The account functions as a proxy with the delegate as its implementation - Delegation persists until explicitly changed - All calls to the account will execute with the delegate's code LBAMM and Apptokens integrate with this model naturally to enhance the protocol's capabilities. --- ## What EIP-7702 Can Do With LBAMM There are many potential use cases for EIP-7702 with LBAMM including: - Smart order routing that fills a hop from multiple pools before proceeding to the next hop. - Advanced partial fills that maintain the excess token amount from each hop. - User accounts as transfer handlers for custom order settlement directly from the user account that holds the tokens. --- ## Executor Model Remains Unchanged In LBAMM: > The executor is always `msg.sender`. With 7702 delegation: - The account remains the executor - Hooks retain the executor context of the actual user No protocol changes are required to maintain the same execution guarantees. --- ## Apptoken Enforcement Remains Intact Apptokens rely on: - Token-level hooks - Executor visibility - Deterministic execution context - Route-aware enforcement EIP-7702 does not weaken any of these guarantees. Because the account remains the executor and Apptokens are delegate-aware: - Token hooks can restrict or allow the executor - Apptokens can restrict or allow the delegate code - Executor-based policy remains enforceable - Recipient restrictions remain enforceable - Handler restrictions remain enforceable Delegation does not bypass enforcement. --- ## What Does Not Change EIP-7702 does not modify: - Pool math or liquidity semantics - Hook ordering - Fee ordering - Partial fill behavior - `singleSwap`, `multiSwap`, or `directSwap` semantics - multi-hop routing rules - Exchange fee or fee-on-top rules All LBAMM invariants remain identical. --- ## Summary EIP-7702 makes accounts programmable through persistent delegation. Within LBAMM: - The user remains the executor. - Execution semantics remain deterministic. - Apptoken enforcement remains fully intact. - No AMM invariants change. LBAMM’s identity-based execution model is directly compatible with persistent delegation. # Orders In LBAMM, every execution is an order match. A swap submitted to the protocol is a **taker order**. Liquidity available to satisfy that swap represents **maker orders**. LBAMM exists to deterministically match the two. --- ## Taker Orders A taker order expresses intent: - “I am willing to provide this token” - “I want to receive that token” - “Here are my exact constraints” The taker defines whether the swap is: - Input-based (swap by input amount) - Output-based (swap by output amount) Nothing in the system executes passively. All state transitions begin with a taker order. --- ## Maker Orders Maker orders represent liquidity made available to satisfy taker intent. Maker liquidity in LBAMM can take multiple forms: - Continuous liquidity exposed through pools - Discrete limit-style commitments - Signed offchain authorizations The protocol does not assume a single liquidity model. Instead, it treats all liquidity expressions as maker orders that can be matched against a taker’s constraints. --- ## Matching Model Matching in LBAMM is: - Deterministic - Single-path - Explicitly routed There is no internal auction and no hidden competition inside execution. A taker order specifies how it should be matched. The protocol executes that path exactly. --- ## A Shared Settlement Layer LBAMM separates: - **Intent** (taker order) - **Liquidity expression** (maker order) - **Settlement guarantees** (core AMM) Liquidity modules, transfer handlers, and hooks may exist as independent contracts. What unifies them is not implementation — it is settlement. All maker expressions, regardless of how they are implemented, ultimately resolve through the same deterministic matching and accounting guarantees provided by the AMM core. --- In the following pages, we’ll explore: - How swaps are structured - How single-hop and multi-hop execution differs - How partial fills are defined - How executors and solvers interact with the protocol # Pool Swaps A **pool swap** is the canonical taker order in LBAMM. When a user calls `singleSwap` or `multiSwap`, they are submitting a taker order that matches against: - Continuous maker liquidity supplied to pools - Any implicit liquidity made available through pool invariants This page explains how pool swaps work from an integrator perspective. --- # Swap Modes LBAMM supports two swap modes, encoded by the sign of `amountSpecified`: ## Swap By Input - `amountSpecified > 0` - You specify how much `tokenIn` you are sending. - You receive as much `tokenOut` as execution allows. - `limitAmount` = **minimum output required**. ## Swap By Output - `amountSpecified < 0` - You specify how much `tokenOut` you want. - The AMM calculates how much `tokenIn` is required. - `limitAmount` = **maximum input allowed**. The sign convention allows a single struct to represent both swap directions cleanly. --- # SwapOrder Struct ```solidity struct SwapOrder { uint256 deadline; address recipient; int256 amountSpecified; // >0 = swap by input, <0 = swap by output uint256 minAmountSpecified; // minimum amount in partial fill cases uint256 limitAmount; // minOut (input mode) or maxIn (output mode) address tokenIn; address tokenOut; } ``` ### Field Semantics - **deadline** Reverts if `block.timestamp > deadline`. - **recipient** Address that receives the final output tokens. - **amountSpecified** Positive → swap by input Negative → swap by output - **minAmountSpecified** Minimum acceptable filled amount of the specified token in case of partial fill. - **limitAmount** - Input mode → minimum output required. - Output mode → maximum input allowed. - **tokenIn / tokenOut** Defines the swap direction. --- # Fee Structs ```solidity struct BPSFeeWithRecipient { uint16 BPS; address recipient; } struct FlatFeeWithRecipient { uint256 amount; address recipient; } ``` - **exchangeFee** (BPS-based) - Applied to swap notional. - Typically used by frontends or order books. - **feeOnTop** (flat fee) - Additional fixed fee. - Not protected inside swap signature semantics. --- # SwapHooksExtraData ```solidity struct SwapHooksExtraData { bytes tokenInHook; bytes tokenOutHook; bytes poolHook; bytes poolType; } ``` Allows passing hook-specific data for: - Token hooks - Pool hooks - Pool-type logic If unused, pass empty bytes. --- # singleSwap ```solidity function singleSwap( SwapOrder calldata swapOrder, bytes32 poolId, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData calldata swapHooksExtraData, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` Executes a swap against a single pool. ### Behavior - Validates deadline and limits - Executes token + pool hooks - Delegates pricing and invariant logic to the pool type - Collects input tokens (directly or via transfer handler) - Transfers output tokens to recipient --- # multiSwap ```solidity function multiSwap( SwapOrder calldata swapOrder, bytes32[] calldata poolIds, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData[] calldata swapHooksExtraDatas, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` Executes a multi-hop route across multiple pools. Each hop’s output becomes the next hop’s input. --- # Multi-Hop Ordering Rules The `poolIds` array **must be supplied in execution order**. The correct order depends on swap mode. --- ## Swap By Input You must start with the pool that accepts `tokenIn`. Route flows forward: ``` tokenIn → poolIds[0] → intermediate token → poolIds[1] → ... → final pool → tokenOut ``` Example: Swapping A → D through B and C: ``` [A/B pool] → [B/C pool] → [C/D pool] ``` --- ## Swap By Output You must start with the pool that outputs `tokenOut`. Route flows backward from the desired output: ``` tokenOut ← poolIds[0] ← intermediate token ← poolIds[1] ← ... ← final pool ← tokenIn ``` The AMM internally computes required inputs backwards across the route. --- # transferData Both `singleSwap` and `multiSwap` accept: ```solidity bytes transferData ``` This parameter enables **custom transfer handlers**. ## Default Behavior (transferData = empty) If `transferData.length == 0`: - The caller of the AMM must have approved the AMM for `tokenIn`. - The AMM will directly collect the required input amount using `transferFrom`. - No transfer handler is invoked. This is the standard ERC-20 approval flow. --- ## Custom Settlement (transferData ≠ empty) If `transferData.length > 0`: - The first 32 bytes are interpreted as the `transferHandler` address. - The remaining bytes are forwarded to that handler as `transferExtraData`. - The transfer handler is responsible for supplying the required input tokens to the AMM. This enables advanced settlement flows such as: - Permit-based transfers - Onchain order book matching - Custom escrow or custody logic If a transfer handler is used, the AMM does **not** rely on ERC-20 allowance from the caller. --- # Partial Fills Partial fills are allowed **only on the first hop**. If a pool cannot fully satisfy the specified amount: - Execution may partially fill. - The filled amount must be ≥ `minAmountSpecified`. - Subsequent hops must execute fully. This ensures deterministic routing and prevents cascading partial state across hops. --- # Return Values Both `singleSwap` and `multiSwap` return: ```solidity (uint256 amountIn, uint256 amountOut) ``` These values represent: - Total input tokens collected - Total output tokens delivered Fees are reflected in these values. --- # Solidity Example ```solidity SwapOrder memory order = SwapOrder({ deadline: block.timestamp + 300, recipient: msg.sender, amountSpecified: int256(1e18), // swap by input minAmountSpecified: 0, limitAmount: 0.99e18, // minimum output tokenIn: address(tokenA), tokenOut: address(tokenB) }); (uint256 amountIn, uint256 amountOut) = amm.singleSwap( order, poolId, BPSFeeWithRecipient({BPS: 0, recipient: address(0)}), FlatFeeWithRecipient({amount: 0, recipient: address(0)}), SwapHooksExtraData("", "", "", ""), "" ); ``` --- # viem Example ```ts await publicClient.writeContract({ address: ammAddress, abi: ammAbi, functionName: 'singleSwap', args: [ { deadline: BigInt(Math.floor(Date.now() / 1000) + 300), recipient: userAddress, amountSpecified: 10n ** 18n, minAmountSpecified: 0n, limitAmount: 99n * 10n ** 16n, tokenIn: tokenA, tokenOut: tokenB, }, { BPS: 0, recipient: zeroAddress }, { amount: 0n, recipient: zeroAddress }, { tokenInHook: '0x', tokenOutHook: '0x', poolHook: '0x', poolType: '0x' }, '0x' ], }); ``` --- Pool swaps represent the most direct taker interaction with LBAMM. They match against continuous maker liquidity while preserving full extensibility through hooks and transfer handlers. # CLOB Transfer Handler The **CLOBTransferHandler** is a transfer handler that implements an onchain central-limit order book (CLOB) and settles AMM swaps by **filling CLOB orders**. Conceptually: * Order makers deposit tokens into the handler and open limit orders at a price. * When an AMM swap uses the CLOBTransferHandler, the AMM sends the swap’s output token to the handler. * The handler uses that output to fill maker orders (crediting makers internally), and then transfers the required input token back to the AMM to settle the swap. This page documents: * How the CLOB integrates with LBAMM swap settlement * Order book identity (orderBookKey / groupKey) * Maker lifecycle (deposit → open → close → withdraw) * Fill path via `ammHandleTransfer` and refund semantics * Hook and token-hook validation surfaces * Events and how indexers can maintain derived state Deep details (order matching math, rounding, and onchain data structures) are described at the level needed for integrators to safely use and index the system. --- ## Key design constraints ### Recipient must be the handler Swaps settled through the CLOBTransferHandler must set: * `swapOrder.recipient == address(CLOBTransferHandler)` The handler requires custody of the AMM’s output tokens in order to: * credit filled makers’ balances in the output token, and * retain those funds in the handler so makers can withdraw later or open new orders in the opposite direction. ### Output-based swaps are not supported CLOB settlement only supports input-based swaps (`swapOrder.amountSpecified >= 0`). If an output-based swap is attempted, `ammHandleTransfer` reverts. --- ## Order books: `orderBookKey` and `groupKey` Each order book is uniquely identified by an `orderBookKey`: ```solidity orderBookKey = keccak256(tokenIn, tokenOut, groupKey) ``` Where `groupKey` encodes: * a CLOB hook address * a minimum order size (base + scale) ### groupKey layout ```solidity bytes32 groupKey = bytes32(uint256(uint160(hook)) << 96) | bytes32(uint256(minimumOrderBase) << 8) | bytes32(uint256(minimumOrderScale)); ``` Helpers exist to decode: * `hook` * `minimumOrder = minimumOrderBase * 10^minimumOrderScale` > `minimumOrderScale` is bounded (max 72) to avoid overflow. ### Initializing orderBookKey metadata Order books are lazily initialized on first use, but an explicit helper exists: * `initializeOrderBookKey(tokenIn, tokenOut, hook, minimumOrderBase, minimumOrderScale)` Once initialized, a public mapping exposes the decoded parameters: * `orderBookKeys[orderBookKey] -> OrderBookKey` --- ## Pricing model Orders are placed at a price represented as `sqrtPriceX96` (Q96 fixed point): * Price is encoded as `sqrt(price) * 2^96` The handler computes tokenOut required for a given tokenIn at a price via: ```solidity function calculateFixedInput(uint256 amountIn, uint160 sqrtPriceX96) internal pure returns (uint256 amountOut) { amountOut = FullMath.mulDivRoundingUp(amountIn, sqrtPriceX96, Q96); amountOut = FullMath.mulDivRoundingUp(amountOut, sqrtPriceX96, Q96); } ``` Notes: * This effectively applies the squared price (in Q96) with rounding up. * `orderAmount` is always denominated in **tokenIn**. ### “Best price for taker” When filling, the handler fills starting from the **best price for the taker** (the AMM swap executor): * the price that yields **the most tokenIn** for **the least tokenOut** At a given price level, orders fill **FIFO**. --- ## Maker lifecycle ### Deposit Makers can pre-fund balances: * `depositToken(token, amount)` The handler tracks balances in: * `makerTokenBalance[token][maker]` A deposit: * transfers tokens into the handler * increments the maker’s internal balance * emits `TokenDeposited` ### Withdraw Makers can withdraw available balances: * `withdrawToken(token, amount)` A withdraw: * decrements the maker’s internal balance * transfers tokens out * emits `TokenWithdrawn` > Non-standard ERC-20 behavior (e.g., rebasing or external balance adjustment) can break the “1:1 backing” assumption and cause drift between internal balances and the handler’s actual token balances. ### Open a limit order Makers open orders via: * `openOrder(tokenIn, tokenOut, sqrtPriceX96, orderAmount, groupKey, hintSqrtPriceX96, hookData)` Key behaviors: * `orderAmount` is funded from existing internal balance; if insufficient, the handler attempts to `transferFrom` the shortfall from the maker. * `orderAmount` must be ≥ `minimumOrder(groupKey)`. * If the group hook is non-zero, the handler calls `ICLOBHook.validateMaker(...)`. * If token hooks have handler-order validation enabled, the handler calls `tokenHook.validateHandlerOrder(...)` for tokenIn and/or tokenOut. * The order is inserted into the per-price FIFO queue. ### Close an order Makers close an order via: * `closeOrder(tokenIn, tokenOut, sqrtPriceX96, orderNonce, groupKey)` Closing: * marks the order closed * returns the **unfilled tokenIn** amount back to the maker’s internal balance * emits `OrderClosed` --- ## Swap settlement via `ammHandleTransfer` When a swap uses the CLOBTransferHandler, the AMM calls: * `ammHandleTransfer(executor, swapOrder, amountIn, amountOut, exchangeFee, feeOnTop, transferExtraData)` ### Preconditions enforced by the handler The handler reverts unless: * `msg.sender == AMM` * `transferExtraData.length > 0` * `swapOrder.recipient == address(this)` * `swapOrder.amountSpecified >= 0` (input-based) ### transferExtraData format `transferExtraData` must decode as: ```solidity struct FillParams { bytes32 groupKey; uint256 maxOutputSlippage; bytes hookData; } ``` The handler derives the order book: * `orderBookKey = hash(tokenIn, tokenOut, groupKey)` ### Taker (executor) validation via group hook If `groupKey` encodes a non-zero hook, the handler calls: * `ICLOBHook(hook).validateExecutor(orderBookKey, executor, swapOrder, amountIn, amountOut, exchangeFee, feeOnTop, hookData)` This is the CLOB’s primary surface for validating takers/executors. ### Fill algorithm (high-level) The handler calls into the matching engine: * `CLOBHelper.fillOrder(orderBook, makerTokenBalance[tokenOut], amountIn, amountOut)` Filling behavior: * Orders fill from the current best price upward until the required `amountIn` is fully sourced. * At each price level, orders fill FIFO. * As each maker order consumes `tokenOut`, the maker’s internal balance in `tokenOut` is incremented. The helper returns: * `fillOutputRemaining`: amount of AMM output `tokenOut` not needed to fill the required input * `endingOrderNonce`, `endingOrderInputRemaining`: head-of-book details after filling The handler emits: * `OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining)` ### Why “output remaining” exists An AMM swap can produce more `tokenOut` than the CLOB needs to source the required `amountIn`. This typically happens when: * the CLOB’s available prices have moved materially since the swap was constructed, so fewer orders (or better prices) are available than implied by the caller’s assumptions. ### Slippage bound for unused output If `fillOutputRemaining > 0`, the handler compares it to `maxOutputSlippage`: * if `fillOutputRemaining > maxOutputSlippage`: revert * otherwise: the handler schedules a refund of `fillOutputRemaining` to the executor This parameter bounds how much “unused output” the executor is willing to tolerate before re-submitting with updated assumptions. ### Refund callback after swap finalization If a refund is required, the handler returns callback data: * `afterSwapRefund(executor, tokenOut, fillOutputRemaining)` After swap finalization, the AMM executes this callback on the handler. Refund behavior: * For wrapped native, the handler attempts to unwrap and send native value to the executor; if unwrap fails it transfers wrapped native. * For ERC-20, it transfers the token directly. ### Settling the AMM After filling, the handler transfers the required input token to the AMM: * `safeTransfer(tokenIn, AMM, amountIn)` If the transfer fails, the handler reverts. --- ## Validation surfaces ### CLOB group hook (`ICLOBHook`) A group hook can enforce: * maker eligibility and constraints at order open * executor/taker eligibility and constraints at fill time Interface: ```solidity interface ICLOBHook is ITransferHandlerExecutorValidation { function validateMaker( bytes32 orderBookKey, address depositor, uint160 sqrtPriceX96, uint256 orderAmount, bytes calldata hookData ) external; } ``` ### Token hook `validateHandlerOrder` (order open only) When opening an order, the handler checks token settings in the AMM: * if `TOKEN_SETTINGS_HANDLER_ORDER_VALIDATE_FLAG` is enabled for tokenIn and/or tokenOut, it calls the token hook: ```solidity validateHandlerOrder( maker, hookForTokenIn, tokenIn, tokenOut, amountIn, amountOut, handlerOrderParams, hookData ) ``` For CLOB orders, `handlerOrderParams` is: ```solidity abi.encode(orderBookKey, sqrtPriceX96) ``` This hook is invoked **only on order open** (not during fills or closes). --- ## Internal order identifiers Within a price bucket, FIFO ordering uses an internal `orderId` derived from the order’s storage slot. ```solidity function _orderToOrderId(Order storage ptrOrder) internal pure returns (bytes32 orderId) { assembly ("memory-safe") { orderId := ptrOrder.slot } } function _orderIdToOrder(bytes32 orderId) internal pure returns (Order storage ptrOrder) { assembly ("memory-safe") { ptrOrder.slot := orderId } } ``` This is an internal implementation detail for maintaining linked lists; integrators should treat `orderNonce` as the public identifier for maker actions. --- ## Events and indexing CLOBTransferHandler is designed to be indexable using events. An indexer can maintain derived state for: * maker balances (by token) * open orders per order book and price * order book head pointer (best price and next order) ### Event reference ```solidity event TokenDeposited(address indexed token, address indexed depositor, uint256 amount); event TokenWithdrawn(address indexed token, address indexed depositor, uint256 amount); event OrderBookInitialized( bytes32 indexed orderBookKey, address tokenIn, address tokenOut, address hook, uint16 minimumOrderBase, uint8 minimumOrderScale ); event OrderOpened( address indexed maker, bytes32 indexed orderBookKey, uint256 orderAmount, uint160 sqrtPriceX96, uint256 orderNonce ); event OrderClosed( address indexed maker, bytes32 indexed orderBookKey, uint256 unfilledInputAmount, uint256 orderNonce ); event OrderBookFill( bytes32 indexed orderBookKey, uint256 endingOrderNonce, uint256 endingOrderInputRemaining ); ``` ### Suggested indexing strategy #### Discover order books Listen for `OrderBookInitialized(orderBookKey, ...)`. * This yields the canonical `tokenIn`, `tokenOut`, `hook`, and minimum-size parameters for the order book. * Order books may also be created lazily; if you observe `OrderOpened`/`OrderBookFill` for an unknown key, you can query `orderBookKeys[orderBookKey]` to resolve metadata (if initialized). #### Track maker balances (derived) For a best-effort derived ledger: * On `TokenDeposited(token, maker, amount)`: `balance[token][maker] += amount` * On `TokenWithdrawn(token, maker, amount)`: `balance[token][maker] -= amount` Order events imply additional balance movements: * On `OrderOpened(maker, orderBookKey, orderAmount, ...)`: * `tokenIn = orderBookKeys[orderBookKey].tokenIn` * `balance[tokenIn][maker] -= orderAmount` * (Note: the handler may have pulled additional tokens from the maker if their internal balance was insufficient. That extra pull emits `TokenDeposited(tokenIn, maker, depositRequired)` inside `openOrder`.) * On `OrderClosed(maker, orderBookKey, unfilledInputAmount, orderNonce)`: * `tokenIn = orderBookKeys[orderBookKey].tokenIn` * `balance[tokenIn][maker] += unfilledInputAmount` * On `OrderBookFill(orderBookKey, ...)`: * makers’ `tokenOut` balances are credited internally as fills occur. * The handler does **not** emit per-maker fill events in this version. An indexer cannot attribute fills to individual makers using events alone. Because fills are aggregated, a purely event-driven indexer **can** still infer which orders were fully filled by combining: * `OrderOpened` (which provides `sqrtPriceX96` and `orderNonce`), and * `OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining)` (which provides the new head order) Since the ending head price can be resolved from the maintained order book state (see below), after each `OrderBookFill` an indexer can conclude: * all price levels strictly better than the ending head price have been fully consumed, and * within the ending head price bucket, all orders with nonces lower than `endingOrderNonce` have been fully consumed, and * the head order at `endingOrderNonce` has `endingOrderInputRemaining` remaining. **Empty book sentinel:** when both `endingOrderNonce` and `endingOrderInputRemaining` are zero, the order book has been completely cleared by the fill. This allows an event-driven indexer to deterministically attribute fills to makers: all orders strictly before the ending head (by price, then FIFO nonce within price) are fully filled, and the ending head order—if non-zero—represents the only partially filled order in that bucket. #### Track open orders From events alone: * You can track the set of opened orders by `(orderBookKey, maker, orderNonce, sqrtPriceX96, orderAmount)` using `OrderOpened`. * You can mark an order closed using `OrderClosed(maker, orderBookKey, ..., orderNonce)`. However, you cannot infer partial fills from events alone. To present “remaining size” for an open order, an indexer must query onchain storage or rely on future events. #### Track head-of-book (best price) `OrderBookFill(orderBookKey, endingOrderNonce, endingOrderInputRemaining)` provides the new head order after a fill. To interpret it fully, the indexer needs the **ending head price**. There are two practical approaches: * **Event + offchain reconstruction (recommended):** maintain per-order-book price buckets from `OrderOpened` and advance the “best price” pointer deterministically as fills occur. * Since the book fills FIFO at the current best price and then moves monotonically to the next worse price until the required input is sourced, the head price after a fill is the lowest price that still has remaining orders. * **Onchain query:** query `orderBookKeys[orderBookKey]` for metadata and read the handler state if you maintain a stateful indexer that can access contract storage. --- ## Integration notes * Swaps settled via CLOB must set the recipient to the handler. * Only input-based swaps are supported. * `maxOutputSlippage` bounds how much unused AMM output can be refunded rather than consumed. * Token hook `validateHandlerOrder` is invoked only when opening orders, and only if enabled in token settings. # Custom Transfer Handlers Custom transfer handlers let developers extend **how swap input tokens are supplied** to LBAMM during swap finalization. A transfer handler is *not* a router. It is a settlement module invoked by the AMM core to obtain the swap’s input tokens, while receiving full context about the swap that just executed. This page describes the interface, the `transferData` encoding used by LBAMM, callback behavior, and the most important safety constraints for building handlers that are compatible with hooks and integrators. --- ## Required interface Custom handlers *must* implement [ILimitBreakAMMTransferHandler](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMTransferHandler.sol). --- ## Call timing and callback flow `ammHandleTransfer` is called by the AMM core **during swap finalization**. Handlers may return `callbackData`. If non-empty, the AMM will execute that callback on the handler **after** the swap finalizes. Key points: * The callback is a normal call to the handler (not a delegatecall). * The AMM does not (and cannot) enforce any handler-internal authorization checks; handlers should validate `msg.sender == AMM` if they require that property. If your handler does not need a post-finalization callback, return empty bytes. --- ## Relationship to token hooks Token hooks can observe which handler is used via `SwapContext.transferHandler`. Important implications: * Tokens may restrict which handlers they allow. * If your handler changes which party is effectively “on the other side” of settlement (e.g., masking the maker/taker), some tokens may require additional validation. ### `validateHandlerOrder` is handler-controlled Token hooks define: * `validateHandlerOrder(...)` This function is **not called by the AMM core**. A custom handler may choose to call it (e.g., during order creation) if the token expects handler-level validation. This decision is part of the handler’s compatibility contract with token hooks and integrators. --- ## Safety and correctness requirements ### Restrict who can call `ammHandleTransfer` Handlers should generally require: * `msg.sender == AMM` The AMM cannot enforce handler-internal access control; this is part of the handler’s responsibility. ### Don’t trust `transferExtraData` * Validate length and decoding * Validate any embedded addresses (e.g., recipient / maker) * Validate any slippage bounds encoded in extra data ### Settlement invariants At minimum, the handler must ensure: * the AMM receives the required `amountIn` of `swapOrder.tokenIn` by the end of handler execution * no party can cause the handler to overpay relative to its authorization model ### Executor identity matters LBAMM treats `executor` as the authoritative identity for the swap. If your handler introduces a separate authorization layer (permits, signatures, whitelists), bind it to `executor` explicitly if required. ### Handle non-standard ERC-20 behavior If your handler maintains internal balances, be aware that non-standard tokens (rebasing, fee-on-transfer, external balance adjustments) can cause drift between internal accounting and onchain balances. --- ## Minimal integration checklist When shipping a custom transfer handler, make sure you can answer: * What bytes format does my handler expect in `transferExtraData`? * Does my handler ever require `swapOrder.recipient == address(this)`? * What authorization model is used (signature, deposit, allowlist, hook)? * How does it bind or validate `executor`? * Does it need a post-finalization callback? If so, what does it do? * Does it call token hook `validateHandlerOrder` at any point? If yes, what is the schema of `handlerOrderParams`? # Transfer Handlers Transfer handlers are modular settlement components used by LBAMM to expand **how swap input tokens are supplied** to the AMM while preserving full execution context. They are primarily about **settlement modularity**: different applications can settle the *same* swap semantics using different mechanisms (direct ERC-20 transfer, permit-based authorization, onchain order books, etc.) without changing pool logic. --- ## What is a transfer handler? A transfer handler is a contract invoked by the AMM core during swap finalization to obtain the input tokens required to settle a swap. Transfer handlers: * Are optional per swap * Can be used with `singleSwap`, `multiSwap`, and `directSwap` * Receive full swap context (executor, order, amounts, and fee configuration) --- ## How a swap selects a transfer handler Each swap entrypoint accepts a `bytes transferData` parameter. * If `transferData.length < 32`: no transfer handler is used (`transferHandler = address(0)`). * If `transferData.length >= 32`: * The first 32 bytes are interpreted as an ABI-encoded transfer handler address * The remaining bytes are treated as `transferExtraData` and passed to the handler Conceptually: ```solidity address transferHandler; bytes transferExtraData; if (transferData.length >= 32) { transferHandler = abi.decode(transferData[:32], (address)); transferExtraData = transferData[32:]; } else { transferHandler = address(0); transferExtraData = ""; } ``` The chosen transfer handler address is surfaced to hooks via `SwapContext.transferHandler`. ## Permissionless extensibility LBAMM ships with two first-party transfer handlers as examples: * A permit-based handler (offchain signed intent) * A CLOB-style handler (onchain orders at specific prices) Developers may deploy additional transfer handlers permissionlessly, provided they implement the required interface. --- ## What transfer handlers are for Transfer handlers exist to make settlement modular while preserving the same AMM execution semantics. Typical uses include: * Alternative authorization and settlement flows for supplying input tokens * Aggregating liquidity or intent sources behind a uniform swap interface * Order-style settlement where the AMM needs extra context to pull funds Importantly, transfer handlers do not change: * pool pricing logic * swap semantics * hook atomicity (a revert still reverts the swap) They only change how the input tokens are obtained for settlement. # Permit Transfer Handler The **PermitTransferHandler** is a transfer handler that settles swap input tokens using **PermitC-style permits**. It allows a swap executor to supply input tokens to the AMM without relying on prior ERC-20 approvals, while binding the authorization to specific swap intent. This handler supports two permit modes: * **Fill-or-kill permits**: must be executed atomically for the exact filled amount * **Partial-fill permits**: may be filled incrementally, with onchain fill-state tracked by PermitC This page describes: * When to use the PermitTransferHandler * How `transferData` / `transferExtraData` are encoded * What the permit signature is bound to * Cosigner and onchain executor validation * How to construct swaps and signatures (Solidity + viem) --- ## What PermitTransferHandler does During swap finalization, the AMM calls the PermitTransferHandler to supply the required input tokens using a PermitC-style permit. At a high level, the handler: * transfers the final net `amountIn` of `swapOrder.tokenIn` into the AMM * validates a PermitC signature bound to swap intent via an additional data hash * enforces atomic fill (fill-or-kill) or bounded incremental fill (partial fill) The handler is compatible with any permit processor that implements the PermitC interface expected by the handler. --- ## Encoding: `transferData` and `transferExtraData` All LBAMM swap entrypoints accept `bytes transferData`. PermitTransferHandler uses it as follows: * The first **32 bytes** are `abi.encode(address(transferHandler))` * The remaining bytes are passed as `transferExtraData` to the handler For PermitTransferHandler, `transferExtraData` is: * **1 byte** permit type discriminator * followed by `abi.encode(...)` of the corresponding permit struct ### Permit type discriminators ```solidity bytes1 constant FILL_OR_KILL_PERMIT = 0x00; bytes1 constant PARTIAL_FILL_PERMIT = 0x01; ``` ### Permit structs ```solidity struct FillOrKillPermitTransfer { address permitProcessor; address from; uint256 nonce; uint256 permitAmount; uint256 expiration; bytes signature; address cosigner; uint256 cosignatureExpiration; bytes cosignature; address hook; bytes hookData; } struct PartialFillPermitTransfer { address permitProcessor; address from; uint256 salt; int256 permitAmountSpecified; uint256 permitLimitAmount; uint256 expiration; bytes signature; address cosigner; uint256 cosignatureExpiration; uint256 cosignatureNonce; bytes cosignature; address hook; bytes hookData; } ``` --- ## Fill-or-kill vs partial-fill semantics ### Fill-or-kill permits A fill-or-kill permit must match the swap’s filled amount exactly: * If the swap is output-based (`swapOrder.amountSpecified < 0`), the filled `amountOut` must equal `-swapOrder.amountSpecified`. * If the swap is input-based (`swapOrder.amountSpecified > 0`), the filled `amountIn` must equal `swapOrder.amountSpecified`. If the swap does not fill exactly as specified, the handler reverts. ### Partial-fill permits Partial-fill permits support incremental fills with PermitC tracking fill state. The handler additionally enforces that: * The permit’s swap mode (input vs output based) matches the swap order’s mode * The partial fill does not exceed the maximum allowed input for the realized output This prevents executors from over-consuming `tokenIn` relative to the signed intent. --- ## What the signature is bound to PermitTransferHandler uses PermitC permits that validate an **additional data hash** derived from swap intent. Conceptually, the hash is built over the following EIP-712 `Swap` payload: ```solidity struct Swap { bool partialFill; address recipient; int256 amountSpecified; uint256 limitAmount; address tokenOut; address exchangeFeeRecipient; uint16 exchangeFeeBPS; address cosigner; address hook; } ``` The hash is included in the PermitC approval type via the following typehash construction: ```solidity PERMITTED_TRANSFER_APPROVAL_TYPEHASH = keccak256( bytes.concat( bytes(PERMITTED_TRANSFER_APPROVAL_TYPEHASH_STUB), bytes(PERMITTED_APPROVAL_TYPEHASH_EXTRADATA_STUB), bytes(SWAP_TYPEHASH_STUB) ) ); PERMITTED_ORDER_APPROVAL_TYPEHASH = keccak256( bytes.concat( bytes(PERMITTED_ORDER_APPROVAL_TYPEHASH_STUB), bytes(PERMITTED_APPROVAL_TYPEHASH_EXTRADATA_STUB), bytes(SWAP_TYPEHASH_STUB) ) ); ``` --- ## Executor authorization: cosigner vs onchain hook PermitTransferHandler supports two optional layers of executor authorization. ### Cosigner (offchain authorization) A permit may include a `cosigner` and `cosignature`. * If `cosigner == address(0)`, cosigner validation is skipped. * Otherwise, the cosignature binds the executor to the permit signature hash. This enables **gasless cancellation**: withholding the cosignature can prevent further execution. Cosignature nonces: * nonce `0` is reusable until filled/expired or canceled in PermitC * non-zero nonces are consumed on first use and cannot be reused ### Executor validation hook (onchain authorization) A permit may include a `hook` implementing an executor validation interface. * If `hook == address(0)`, hook validation is skipped. * Otherwise, the handler calls the hook with the additional data hash and full swap context. This enables onchain authorization policies that need more context than an offchain cosignature. --- ## Solidity: building `transferData` The AMM expects `transferData` to begin with the 32-byte ABI encoding of the handler address. ```solidity function encodeTransferData( address permitTransferHandler, bytes1 permitType, bytes memory abiEncodedPermitStruct ) pure returns (bytes memory transferData) { // transferData := abi.encode(handler) || (permitType || abi.encode(permitStruct)) bytes memory transferExtraData = bytes.concat(permitType, abiEncodedPermitStruct); return bytes.concat(abi.encode(permitTransferHandler), transferExtraData); } ``` Example (fill-or-kill): ```solidity bytes memory permitStruct = abi.encode( FillOrKillPermitTransfer({ permitProcessor: permitC, from: maker, nonce: nonce, permitAmount: permitAmount, expiration: expiration, signature: permitSignature, cosigner: cosigner, cosignatureExpiration: cosigExp, cosignature: cosig, hook: validationHook, hookData: hookData }) ); bytes memory transferData = encodeTransferData( address(PERMIT_TRANSFER_HANDLER), FILL_OR_KILL_PERMIT, permitStruct ); ``` --- ## viem: encoding `transferData` Below is a minimal viem-style approach for constructing the same bytes layout. ```ts import { encodeAbiParameters, parseAbiParameters, concatHex, padHex, toHex } from 'viem' export function encodeTransferDataPermit( handler: `0x${string}`, permitType: 'fillOrKill' | 'partialFill', encodedPermitStruct: `0x${string}`, ): `0x${string}` { // First 32 bytes: abi.encode(address(handler)) const handlerWord = encodeAbiParameters(parseAbiParameters('address'), [handler]) // 1 byte discriminator const typeByte = permitType === 'fillOrKill' ? '0x00' : '0x01' // transferExtraData := typeByte || encodedPermitStruct const transferExtraData = concatHex([typeByte as `0x${string}`, encodedPermitStruct]) // transferData := handlerWord || transferExtraData return concatHex([handlerWord, transferExtraData]) } ``` > `encodedPermitStruct` should be the ABI encoding of `FillOrKillPermitTransfer` or `PartialFillPermitTransfer`. --- ## Signing permits (PermitC) PermitTransferHandler uses PermitC-style EIP-712 signatures. Two permit families are used: * Fill-or-kill: `PermitTransferFromWithAdditionalData(...)` * Partial fill: `PermitOrderWithAdditionalData(...)` Both incorporate an **additional data hash** whose type includes a `Swap` struct (conceptually): * `partialFill` * `recipient` * `amountSpecified` / `limitAmount` * `tokenOut` * exchange fee recipient and BPS * `cosigner` * `hook` The EIP-712 domain separator comes from the specific PermitC instance (`permitProcessor`) selected in the permit data. ### Practical guidance * Prefer building signatures using the PermitC SDK/tools for the chosen PermitC version. * Ensure the same fields are used when building the swap additional data hash; the PermitTransferHandler will recompute it and PermitC will validate it. > This documentation intentionally avoids re-specifying the full PermitC type layouts. Treat PermitC as the source of truth for the signing schema. --- ## Integration notes * The permit holder (`from`) may be different from the swap executor. The permit signature authorizes the transfer from `from` to the AMM. * Multi-hop swaps still use a single final net `amountIn` pull of the route’s overall input token. * The handler does not use `callbackData` (it returns empty callback data). # Economic Design Patterns This page describes compositional economic patterns enabled by LBAMM’s fee surfaces. These are not recommendations. They are examples of how different fee surfaces can be combined to produce specific economic behavior. Each pattern is described in terms of: - Fee surfaces used - Value flow - Enforcement boundary - Risks / invariants --- ## Executor-Compensated Routing ### Goal Allow third-party executors (solvers, searchers, relayers) to route orders and be paid deterministically at execution time. ### Surfaces Used - Fee-on-top (execution-level) - Optional exchange fee - Token hooks (optional restrictions) ### Structure The executor fee is encoded as: ```solidity FlatFeeWithRecipient({ amount: executorFee, recipient: executor }) ```` This ensures: * Executor compensation is explicit * Compensation is paid in `tokenIn` * No hidden reward surface exists ### Invariants * Fee-on-top must satisfy swap bounds. * Execution-level fee applies once (not per hop). * If a transfer handler enforces signature integrity (e.g., permit-based handler), fee parameters may be constrained by signed order data. --- ## Exchange Revenue Model ### Goal Capture a proportional fee for a DEX frontend or routing layer. ### Surfaces Used * Exchange fee (BPS-based) ### Structure ```solidity BPSFeeWithRecipient({ BPS: exchangeBps, recipient: exchangeTreasury }) ``` Exchange fee: * Is applied to execution-level input * Scales automatically in swap-by-output mode * Is independent of LP fees ### Invariants * `recipient == address(0)` requires `BPS == 0` * BPS must satisfy mode-specific bounds * Does not compound across hops --- ## Token-Controlled Markets ### Goal Allow a token issuer to enforce economic policy across all swaps. ### Surfaces Used * Token hook swap fees * Token hook liquidity validation * Optional minimum protocol hop fees ### Structure Token hook may: * Charge dynamic swap fees via `beforeSwap` / `afterSwap` * Restrict swap direction or participants * Enforce liquidity hook usage Because token hooks observe: * Executor * Recipient * Hop index * PoolId They can implement: * Tiered fee models * Loyalty pricing * Whitelist discounts * Cross-pool invariants ### Invariants * Hook fees accrue in core. * Excessive hook fees may cause swap limit reverts. * Enforcement applies per hop. --- ## Liquidity Lock + Performance Fee ### Goal Lock liquidity and assess performance-based extraction. ### Surfaces Used * Position hook * Liquidity operation hook fees ### Structure Position hook: * Prevents removal before unlock condition * Assesses fee during `removeLiquidity` or `collectFees` Hook fees are bounded by: * `maxHookFee0` * `maxHookFee1` Total hook fees are aggregated across all hooks before enforcement. ### Invariants * Hooks are unaware of each other’s returns. * Enforcement applies to aggregated totals. * Withdrawal may be reduced by hook fees. --- ## Pool-Specific Access Markets ### Goal Create a pool that restricts who may provide liquidity or access swaps. ### Surfaces Used * Pool hook validation * Dynamic LP fee selection ### Structure Pool hook may: * Revert unauthorized liquidity additions * Select LP fee dynamically based on execution context * Charge liquidity operation hook fees Because dynamic LP fee selection occurs at swap-time: * Pool can express price discrimination * Fees may vary per executor or route ### Invariants * Dynamic LP fee must satisfy core bounds. * Pool hook cannot modify reserves directly. * Execution remains atomic. --- ## Protocol Revenue Capture ### Goal Allow protocol-level capture of revenue from multiple surfaces. ### Surfaces Used * Protocol fee on LP fees * Protocol fee on exchange fee * Protocol fee on fee-on-top * Minimum hop fee configuration Protocol fee configuration may be: * Global * Overridden per poolId * Overridden per exchange fee recipient * Overridden per fee-on-top recipient ### Invariants * Defaults may be zero. * Protocol fee rates are queryable. * Protocol fee events are emitted when taken. * Protocol fee does not alter ordering semantics. --- ## Affiliate Routing Model ### Goal Reward referrers or affiliates without altering pool math. ### Surfaces Used * Fee-on-top * Exchange fee (split via multiple routes) * Token hook contextual logic Structure options: 1. Affiliate as `feeOnTop.recipient` 2. Affiliate as exchange fee recipient 3. Affiliate logic embedded in token hook This allows: * Explicit affiliate compensation * No changes to pool-level pricing * Deterministic visibility of payout --- ## Compliance-Gated Liquidity ### Goal Allow liquidity provision or swaps only under specific compliance conditions. ### Surfaces Used * Token hooks * Position hooks * Pool hooks Hooks may: * Revert on unauthorized executor * Enforce use of a specific liquidity hook * Charge additional fees for non-compliant actors Economic control becomes programmable policy. --- ## Multi-Hop Controlled Markets ### Goal Ensure fee behavior is stable across routes. ### Surfaces Used * Execution-level fees (once) * Per-hop LP fees * Per-hop token hook fees Design principle: * Exchange fee and fee-on-top apply once * LP and token hook swap fees apply per hop This separation ensures: * No compounding of execution-level fees * Predictable total cost modeling * Safe route simulation --- ## Anti-MEV or Route-Specific Pricing ### Goal Charge different economics based on executor identity or route characteristics. ### Surfaces Used * Token hook swap logic * Pool hook dynamic LP fee Hooks observe: * Executor * Hop index * PoolId * Recipient This allows: * MEV-aware fee structures * Route-based pricing adjustments * Contextual rebates --- ## Design Principles Across Patterns Across all patterns: 1. Execution-level fees are applied once. 2. LP fees are hop-scoped. 3. Hook fees accrue and settle post-operation. 4. Liquidity hook fees are bounded by aggregated max limits. 5. All operations remain atomic. 6. No fee surface is implicit. --- ## What This Page Does Not Do This page does not: * Prescribe recommended fee values * Suggest governance structures * Define economic policy It demonstrates composability enabled by the protocol. # Fee Ordering & Settlement This page defines the **deterministic ordering** and **settlement semantics** of fee surfaces in LBAMM. It separates: 1. Execution-level adjustments 2. Hop-level pool mechanics 3. Hook-level fee accrual 4. Final settlement This separation is critical for reasoning about multi-hop swaps and composability. --- # Swap Execution Ordering Swap execution consists of distinct phases. ## Execution-Level Adjustments (Applied Once) Execution-level adjustments apply once per `singleSwap` or `multiSwap` call, regardless of hop count. These include: - Fee-on-top (flat input adjustment) - Exchange fee (BPS-based) ### Swap-by-Input Given an input amount `X`: 1. Fee-on-top is removed from `X` 2. Exchange fee is computed on the remaining input 3. The resulting net input proceeds into hop execution ### Swap-by-Output Given a target output amount: 1. Pool math determines required net input 2. Exchange fee scales input upward 3. Fee-on-top is added to the required input These adjustments: - Do not apply per hop - Do not compound across route segments - Apply only at the execution boundary --- ## Hop-Level Pool Mechanics (Per Hop) After execution-level adjustments: - Swap proceeds hop-by-hop - Each hop: - Applies its LP fee - Updates reserves - Emits `Swap` event - Produces an output used as the next hop’s input Execution-level exchange fee and fee-on-top do not reapply at this layer. --- ## Token Hook Fees (Per Hop) For each hop: 1. `beforeSwap` (if enabled) 2. Pool swap logic 3. `afterSwap` (if enabled) Token hook fees: - Are computed per hop - May cause swap to revert - Are accrued in core - Are not transferred immediately during hook execution There is no explicit max parameter for swap hook fees, but: - Hooks cannot overconsume the trade - Excessive fees may trigger swap limit validation reverts --- # Liquidity Operation Ordering Liquidity operations (`addLiquidity`, `removeLiquidity`, `collectFees`) follow a deterministic sequence. ## Hook Validation Order 1. Token hooks (token0, token1 sides if enabled) 2. Position hook (if provided) 3. Pool hook (if configured) Each hook may: - Revert - Return hook fees --- ## Hook Fee Aggregation For liquidity operations: - All hook fees for `token0` are summed - All hook fees for `token1` are summed - Final totals are compared against: - `maxHookFee0` - `maxHookFee1` Hooks are unaware of each other’s fee returns. Enforcement occurs on the aggregated totals. --- ## Netting Semantics Hook fees may: - Increase required deposit - Reduce withdrawal amount In edge cases: - A withdrawal may be reduced enough that the provider must supply additional funds --- # Flashloan Ordering Flashloan execution: 1. Loan token hook `beforeFlashloan` - Returns `(feeToken, feeAmount)` 2. Optional validation if `feeToken` differs from loan token 3. Loan is issued 4. Loan is repaid 5. Fee is accounted Flashloan fee: - Is accrued using the token-hook fee accounting model - Emitted via `Flashloan` event - Is not subject to explicit protocol cap --- # Settlement Model LBAMM separates: - Calculation - Accrual - Transfer ## Execution Atomicity All operations are atomic: - If any hook reverts, the entire operation reverts - No fee surface partially commits --- ## Exchange Fee and Fee-on-Top Settlement For swaps: - Exchange fee and fee-on-top are transferred by core - If a transfer handler is used: - Handler must supply total input (including fees) - If no handler: - Input tokens are collected from executor directly Exchange fee and fee-on-top are not emitted in swap events today. --- ## Hook Fee Accrual Hook fees: - Are accrued in core balances - May be collected by the hook later - May be queued for collection during execution depending on hook configuration Queued transfers: - Execute after primary operation finalizes - Process FIFO - Cannot interfere with swap/liquidity accounting --- ## Protocol Fee Settlement Protocol fees: - Are taken according to configured rates - May apply to: - Hop-level token fees - LP fees - Exchange fee - Fee-on-top - Are emitted via: ```solidity event ProtocolFeeTaken(address indexed token, uint256 amount); ```` Protocol fee accounting is internal but fee rates are queryable. --- # Multi-Hop Invariants For `multiSwap`: * Exchange fee applies once * Fee-on-top applies once * Transfer handler (if any) cannot change mid-route * LP fees apply per hop * Token hook swap fees apply per hop * Protocol hop minimum fees (if configured) apply per hop These invariants prevent compounding of execution-level fees. --- # Key Invariants Summary * Execution-level fees are applied once. * LP fees are hop-scoped. * Hook fees are accrued and settled post-operation. * Liquidity hook fees are bounded by aggregated max limits. * Flashloan fees are governed by token hook logic. * All operations are atomic. # Fee Surfaces This page enumerates every **fee surface** in LBAMM and defines, for each surface: - **Trigger** (when it can be charged) - **Who defines it** - **Denomination** (which token the fee is paid in) - **Recipient** - **Bounds** - **Settlement semantics** - **Observability** (events / query surfaces) This page is reference-oriented and describes protocol behavior (not recommended strategies). --- ## Summary table > **Terminology:** “Execution-level” means values passed to the top-level swap call (applies once per `singleSwap`/`multiSwap`, not per hop). “Hop-level” means per-pool-hop behavior inside a multi-hop route. | Surface | Scope | Who defines it | Denomination | Recipient | Bounds / validation | Settlement | Observable | |---|---|---|---|---|---|---|---| | **Exchange fee** | Swap (execution-level) | Caller-supplied params (`BPSFeeWithRecipient`) | `tokenIn` | `exchangeFee.recipient` | Swap-by-input: `BPS <= 10_000`; swap-by-output: `BPS < 10_000`; if `recipient==0` then `BPS==0` | Core transfers fee to recipient; handler (if used) supplies total `tokenIn` incl. fees | **Not** in Swap event today | | **Fee-on-top** | Swap (execution-level) | Caller-supplied params (`FlatFeeWithRecipient`) | `tokenIn` | `feeOnTop.recipient` | Swap-by-input: `amount <= amountSpecified`; swap-by-output: no protocol cap; if `recipient==0` then `amount==0` | Core transfers fee to recipient; handler (if used) supplies total `tokenIn` incl. fees | **Not** in Swap event today | | **Executor fee** | Swap (execution-level) | Implemented as **fee-on-top** | `tokenIn` | Executor or designated recipient | Same as fee-on-top | Same as fee-on-top | Same as fee-on-top | | **LP fee** | Swap (hop-level / pool-level) | Pool configuration (fixed) or pool hook (dynamic selection) | Hop’s effective input token for that hop | Liquidity providers (pool accounting) | Core-enforced caps on dynamic selection by swap mode (see pool hook docs) | Accounted inside pool; shown as `lpFeeAmount` in `Swap` event | `Swap` event includes `lpFeeAmount` | | **Token-hook fees (swap)** | Swap (per hop; before/after) | Token hook return values | Per token hook rules (see Token Hooks) | Accrued to hook or attributed per token-hook settings | No explicit “max” param; cannot overconsume between both hooks; excessive fees can cause swap limit checks to revert | Accrued in core; collectible by hook; hooks may queue collection depending on settings | Not in Swap event today | | **Token-hook fees (liquidity)** | Liquidity ops | Token hook return values | `token0` / `token1` | Accrued to hook or attributed per token-hook settings | Subject to `maxHookFee0/1` (total across all hooks) | Accrued in core; may be queued for collection post-op | Reflected indirectly via liquidity events’ `fees0/fees1` fields (see notes) | | **Pool-hook fees (liquidity)** | Liquidity ops | Pool hook return values | `token0` / `token1` | Accrued to pool hook address | Subject to `maxHookFee0/1` (total across all hooks) | Accrued in core; collectible by hook | Reflected indirectly via liquidity events’ `fees0/fees1` fields (see notes) | | **Position-hook fees (liquidity)** | Liquidity ops | Position hook return values | `token0` / `token1` | Accrued to position hook address | Subject to `maxHookFee0/1` (total across all hooks) | Accrued in core; collectible by hook | Reflected indirectly via liquidity events’ `fees0/fees1` fields (see notes) | | **Protocol fees** | Multiple (configurable) | Protocol configuration (global + overrides + per-hop minimums) | Depends on surface (can be taken from hop token / from other fee surfaces) | Protocol | Queryable fee rates; emitted when taken | Accounted internally; emitted when taken | `ProtocolFeeTaken(token, amount)` event | | **Flashloan fee** | Flashloan | Loan token’s flashloan hook | `feeToken` returned by hook (may differ from loan token) | Stored like token hook fees (collectible under same model) | No explicit cap in core; `feeToken` must allow use (via validation) | Accrued in core (token-hook fee model) | `Flashloan` event includes `feeToken` + `feeAmount` | --- ## Execution-level swap fees Execution-level fees are provided to `singleSwap` / `multiSwap` and apply **once per swap call** (even for multi-hop routes). ### Exchange fee (BPS, proportional) **What it is** A percentage fee (basis points) applied to the swap’s **execution-level input**. **Where it’s encoded** ```solidity struct BPSFeeWithRecipient { uint16 BPS; address recipient; } ```` **Bounds / validity** * Swap-by-input: `exchangeFee.BPS <= 10_000` * Swap-by-output: `exchangeFee.BPS < 10_000` * If `exchangeFee.recipient == address(0)`, then `exchangeFee.BPS` **must be 0** **Multi-hop** * Applied once to the execution-level input, **not per hop**. --- ### Fee-on-top (flat amount, additive) **What it is** A flat fee amount denominated in the swap’s **input token**. **Where it’s encoded** ```solidity struct FlatFeeWithRecipient { uint256 amount; address recipient; } ``` **Bounds / validity** * Swap-by-input: `feeOnTop.amount <= amountSpecified` * Swap-by-output: no protocol-level cap on `feeOnTop.amount` * If `feeOnTop.recipient == address(0)`, then `feeOnTop.amount` **must be 0** **Multi-hop** * Applied once to the execution-level input, **not per hop**. --- ### Ordering between exchange fee and fee-on-top Ordering differs by swap mode. #### Swap-by-input * Fee-on-top is removed from input **before** exchange fee is applied. * Exchange fee is computed from the remaining input. #### Swap-by-output * Exchange fee scaling is applied first (input increases to cover BPS fee). * Fee-on-top is added **after** exchange fee. > This ordering is strictly about **execution-level fee adjustments** and should not be confused with per-hop pool mechanics. --- ### Executor fees Executor fees are implemented as **fee-on-top**. * If an executor is compensated at swap-time, that compensation is explicitly encoded as `feeOnTop`. * There is no hidden solver reward surface. --- ### Example: setting execution-level fees in a swap call ```solidity // Execution-level swap fees: BPSFeeWithRecipient memory exchangeFee = BPSFeeWithRecipient({ BPS: 30, // 0.30% recipient: exchangeFeeRecipient }); FlatFeeWithRecipient memory feeOnTop = FlatFeeWithRecipient({ amount: 1e15, // flat fee in tokenIn units recipient: executor // executor fee }); // Validity constraints: require(exchangeFee.recipient != address(0) || exchangeFee.BPS == 0, "invalid exchange fee"); require(feeOnTop.recipient != address(0) || feeOnTop.amount == 0, "invalid fee-on-top"); // For swap-by-input, feeOnTop cannot exceed the specified input amount (pseudo-check). // (Exact check depends on how swap mode is expressed in your SwapOrder.) ``` --- ## Pool-level fees ### LP fee **What it is** The fee charged by a pool’s liquidity model and distributed to liquidity providers. **Where it’s defined** * Fixed in pool configuration, or * Dynamically selected by a pool hook when the pool is created with the dynamic-fee sentinel. **Denomination** * Charged in the hop’s effective input token (hop-level). **Observability** * LP fee amount for a hop emitted during swap: ```solidity event Swap( bytes32 indexed poolId, address indexed recipient, bool zeroForOne, uint256 amountIn, uint256 amountOut, uint256 lpFeeAmount ); ``` * Liquidity events include fee fields for amount collected by a position: ```solidity event LiquidityAdded(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1); event LiquidityRemoved(bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1); event FeesCollected(bytes32 indexed poolId, address indexed provider, uint256 fees0, uint256 fees1); ``` > LP fee is a pool-level mechanism. It is not an execution-level fee adjustment and should not be described as “before/after exchange fee” at the route level. --- ## Hook-level fees Hook-level fees are assessed by hooks (token/pool/position) and are **accrued in core** for later collection. Hooks may be able to queue collection so that distribution occurs post-execution, depending on settings (documented in the Hooks section). ### Token-hook fees on swaps **Trigger** * Token hook `beforeSwap` and/or `afterSwap` (per hop; gated by token settings) **Bounds** * No explicit max parameter for swap hook fees. * Fees cannot overconsume the trade between both hooks. * Excessive hook fees can cause limit validation to revert (swap’s limit constraints). **Settlement** * Accrued to a balance in core; collectible by hook. * Some configurations allow attribution to the token rather than the hook address (token hook settings). > Fee denomination truth tables are defined in the Token Hooks docs; this page treats token swap hook fees as a distinct surface. --- ### Hook fees on liquidity operations (token + position + pool) For liquidity operations, multiple hooks may assess fees: * Token hooks (token0/token1 sides) * Position hook (liquidity hook) * Pool hook **Denomination** * Always `token0` / `token1` of the pool for liquidity operations. **Bounding** * `maxHookFee0` / `maxHookFee1` are bounds on the **total** hook fees (summed across hooks) per token. * Hooks do not coordinate; each returns fees independently. * Enforcement compares the final totals to the max bounds. **Netting semantics** * Hook fees can increase a provider’s required deposit or reduce their withdrawal. * In some cases, withdrawal may be reduced enough that the provider must supply additional funds. **Settlement** * Accrued in core, collectible by the hook(s). * Hooks may queue collection during execution; settlement occurs after the primary operation finalizes. (If you want a stricter definition per event field, we can add it once you confirm the intended semantics.) --- ## Protocol fees LBAMM supports protocol fee configurations at multiple levels, including: * Minimum protocol fees on specific token hops (configured individually; default none) * Global protocol fees on: * LP fees * Exchange fees * Fee-on-top fees (defaults 0) * Overrides for specific: * poolId (LP fee protocol fee) * exchange fee recipient * fee-on-top recipient Because protocol fee utilization may be defaulted to zero initially and may evolve, this page treats protocol fees as a distinct surface at a high level. --- ## Flashloan fees Flashloan fees are defined by the **loan token’s flashloan hook**, which returns: * `feeToken` (may differ from the loan token) * `feeAmount` If `feeToken` differs, the fee token's hook must allow the loan token to use it as a fee currency (via validation). **Settlement** * Stored and collected using the same accounting model as token hook fees. **Observability** ```solidity event Flashloan( address indexed requester, address indexed executor, address indexed loanedToken, uint256 loanAmount, address feeToken, uint256 feeAmount ); ``` --- ## Related pages * **Fee Ordering & Settlement** — deterministic ordering rules and settlement semantics across operations * **Token Hooks / Pool Hooks / Position Hooks** — hook fee denomination, flags, queueing, and collection mechanics * **Pool Types** — LP fee behavior and pool-specific math # Fees & Economics This section defines the **fee surfaces** and **economic semantics** of LBAMM: where fees can be assessed, how they are bounded, and how they settle. LBAMM does not have a single “protocol fee.” Instead, it exposes multiple **first-class fee surfaces** spanning: - **Execution-level swap fees** (exchange fee, fee-on-top) - **Pool-level fees** (LP fees and protocol fee configurations) - **Hook-level fees** (token/pool/position hooks) - **Flash loan fees** The goal of this section is to give integrators and auditors a **deterministic, reference-grade model** for value flow. --- ## Fee surfaces vs. implementation details LBAMM fee surfaces are defined in terms of: - **Scope** (swap, liquidity operation, flash loan) - **Denomination** (which token the fee is paid in) - **Recipient** - **Bounds / caps** - **Settlement semantics** (immediate transfer vs. accrued balance vs. queued settlement) This section intentionally separates: - **Execution-level fee adjustments** (apply once per swap call), from - **Per-hop pool mechanics** (apply within each hop), and from - **Hook fees** (policy-driven and context-aware) This avoids incorrect assumptions in multi-hop routes. --- ## Execution-level swap fees LBAMM supports two execution-level swap fee surfaces: ### Exchange fee (BPS, proportional) The exchange fee is a basis point fee applied to a swap’s **execution-level input**. Key properties: - **Applies once per swap call** (`singleSwap` / `multiSwap`), not per hop. - **Recipient** and **BPS** are supplied as call parameters. - **Output-mode scaling:** for swap-by-output, the required input is scaled up to cover the exchange fee. **Bounds (enforced by core):** - Swap-by-input: `exchangeFee.BPS <= 10_000` - Swap-by-output: `exchangeFee.BPS < 10_000` - If `exchangeFee.recipient == address(0)`, then `exchangeFee.BPS` **must be 0**. --- ### Fee-on-top (flat amount, additive) Fee-on-top is a flat fee amount denominated in the **input token**. Key properties: - **Applies once per swap call**, not per hop. - Supplied as call parameters (`amount`, `recipient`). - Commonly used to implement **executor fees** (see below). - **Output-mode scaling:** for swap-by-output, the required input is increased by the flat fee amount. **Bounds (enforced by core):** - Swap-by-input: `feeOnTop.amount` **cannot exceed** the specified input amount. - Swap-by-output: no protocol-level cap on `feeOnTop.amount` (it is constrained by overall swap limits / feasibility). - If `feeOnTop.recipient == address(0)`, then `feeOnTop.amount` **must be 0**. --- ### Ordering between exchange fee and fee-on-top The exchange fee and fee-on-top have a deterministic relationship that differs by swap mode: #### Swap-by-input - Fee-on-top is **removed from the provided input** before exchange fee is applied. - Exchange fee is applied to the remaining input before swap execution proceeds. #### Swap-by-output - Exchange fee is applied first (scaling up required input). - Fee-on-top is added **after** exchange fee. These ordering rules apply at the **execution level** and do not compound per hop in `multiSwap`. --- ### Executor fees Executor fees are represented as **fee-on-top**. LBAMM does not introduce a hidden executor reward surface. If an executor is compensated at swap-time, it is explicitly encoded as `feeOnTop`. --- ### Notes on transfer handlers (non-normative) If a transfer handler is used, the handler receives the exchange fee and fee-on-top parameters as part of settlement. Some handlers may impose additional constraints on fee fields. For example, a permit-based handler may require that the exchange fee recipient and BPS match what a user signed off-chain for an orderbook/UX flow. This behavior is **handler-specific** and should not be treated as a global rule. (Transfer handler mechanics are documented in the Orders → Transfer Handlers section.) --- ## Hook-level fees Hooks may assess fees during swaps and liquidity operations: - Token hooks (`beforeSwap` / `afterSwap`, and liquidity validation hooks) - Pool hooks (liquidity operation validation, and dynamic LP fee selection in swaps when enabled) - Position hooks (liquidity operation validation) General properties: - Hook calls may revert and therefore block execution. - Hook fees are generally **accrued in core** and later collected. - Some token hook configurations allow fees to be attributed/managed differently (documented in the Hooks section). This section does not redefine hook fee denomination tables; those are specified in the token hook docs. --- ## Pool-level fees and protocol fee configurations LBAMM supports protocol fee configurations that can apply at multiple levels, including: - Minimum fee rates on specific **token hops** (configured per hop; default none) - Global protocol fees on: - LP fees - Exchange fees - Fee-on-top fees (defaults 0) - Overrides for specific: - poolId (LP fee protocol fee) - exchange fee recipient - fee-on-top recipient **Important:** This protocol fee surface is likely to be defaulted to zero initially and may evolve. For now, this section describes it at a high level; details are captured in the Fee Surfaces reference page. --- ## Flash loan fees Flash loan fees are governed by the **loan token’s flashloan hook**, which returns: - `feeToken` (may differ from the loan token) - `feeAmount` If the fee token differs from the loan token, the fee token must allow the loan token to use it as a fee currency (via hook validation). Flash loan fees settle similarly to token hook fees (accrued/managed via the same hook fee accounting model). --- ## Observability The current event surface primarily exposes: - LP fee amounts on swaps - Flash loan fee details - Liquidity add/remove/collect fee fields - Protocol fee taken events Notably, there are **no events** that directly log exchange fee or fee-on-top amounts/recipients today. --- ## What’s next in this section The remainder of this section provides reference pages: - **Fee Surfaces**: an exhaustive breakdown of each fee surface (trigger, denomination, recipient, bounds, settlement, and visibility). - **Fee Ordering & Settlement**: deterministic ordering rules and settlement semantics across swaps and liquidity operations, including multi-hop considerations. - **Economic Design Patterns**: composition patterns enabled by these surfaces (kept separate from the reference spec). # Create a Pool This guide walks through creating a new pool in LBAMM. It assumes: - Tokens are already deployed. - You understand basic pool concepts. - You are interacting with a production deployment. Creating a pool consists of: 1. Preparing `PoolCreationDetails` 2. Optionally preparing hook data 3. Calling `createPool` 4. Handling deterministic `poolId` 5. (Optional) Adding liquidity during creation --- # Pool Identity Is Deterministic Pools in LBAMM are identified by a deterministic `poolId`. `poolId` is generated by the **pool type contract**, subject to constraints enforced by LBAMM core. Pool types must implement: ```solidity function computePoolId( PoolCreationDetails calldata poolCreationDetails ) external view returns (bytes32 poolId); ```` `poolId` layout: ``` Bits 0 to 15 - LP fee in BPS Bits 16 to 143 - creation details hash (may include pool-specific params) Bits 144 to 255 - pool type address (must fit in 112 bits) ``` ### Important * `poolId` can be computed offchain using `computePoolId`. * The same `PoolCreationDetails` always produces the same `poolId`. * LBAMM prevents initializing the same `poolId` twice. * Pool creation is permissionless unless restricted by hooks or custom pool type logic. --- # PoolCreationDetails ```solidity struct PoolCreationDetails { address poolType; uint16 fee; address token0; address token1; address poolHook; bytes poolParams; } ``` ### Field Semantics * **poolType** Address of the pool type module. * **fee** LP fee in BPS (max 10000 = 100%), OR sentinel value of `55555` for dynamic fee. * **token0 / token1** Must satisfy `token0 < token1` for canonical ordering. * **poolHook** Address of pool hook (zero address = no hook). * **poolParams** ABI-encoded initialization parameters specific to the pool type. --- # Token Ordering Rules LBAMM will reorder tokens internally if necessary. However: > You should always supply tokens in sorted order (`token0 < token1`). Incorrect ordering may: * Cause confusion in offchain simulation * Cause confusion when adding liquidity with pool creation Best practice: ```ts const [token0, token1] = tokenA.toLowerCase() < tokenB.toLowerCase() ? [tokenA, tokenB] : [tokenB, tokenA]; ``` --- # LP Fee Configuration ## Fixed Fee Provide BPS directly: ```solidity details.fee = 300; // 3.00% ``` Constraints: * Must be ≤ 10000 * If > 10000 and not 55555 with a non-zero pool hook → revert --- ## Dynamic Fee Use sentinel: ```solidity uint16 constant DYNAMIC_POOL_FEE_BPS = 55_555; ``` ```solidity details.fee = 55_555; ``` Requirements: * `details.poolHook` MUST be non-zero. * If `fee == 55_555` and `poolHook == address(0)` → revert. * Pool hook must implement dynamic fee selection. --- # Hook Validation Order During creation: 1. token0 hook 2. token1 hook 3. pool hook Hooks: * Receive `poolId` * May revert * May enforce custom restrictions Token hooks: * May block pool creation. * Do not charge fees through LBAMM core during creation. Custom pool types may also enforce additional restrictions. --- # Calling `createPool` ```solidity function createPool( PoolCreationDetails memory details, bytes calldata token0HookData, bytes calldata token1HookData, bytes calldata poolHookData, bytes calldata liquidityData ) external returns (bytes32 poolId, uint256 deposit0, uint256 deposit1); ``` ### Parameter Breakdown ### `details` The full `PoolCreationDetails` struct defining: * Pool type * LP fee (or dynamic sentinel) * token0 / token1 * pool hook * Pool-specific initialization parameters This struct deterministically defines the `poolId`. --- ### `token0HookData` Arbitrary calldata passed to **token0’s hook** during pool creation. * Only used if token0 has a hook configured and pool creation triggers validation. * May contain authorization data, configuration parameters, or validation proofs. * If token0 does not use hooks for creation validation, this may be empty (`"0x"`). --- ### `token1HookData` Same semantics as `token0HookData`, but for token1. * Passed directly to token1’s hook during validation. * Ignored if token1 has no relevant hook logic for pool creation. --- ### `poolHookData` Arbitrary calldata passed to the **pool hook** during pool-level validation. * Used only if `details.poolHook` is non-zero. * Must match the expectations of the pool hook contract. * Ignored if no pool hook is configured. Hooks may revert during validation, which will revert the entire pool creation. --- ### `liquidityData` Optional calldata that triggers an immediate `addLiquidity` call **after** pool creation. Requirements: * Must be a valid ABI-encoded call to `addLiquidity`, including the function selector. * **Must target the pool being created.** * Must conform to the pool type’s expected liquidity parameters. * If malformed or if the `addLiquidity` call reverts → the entire transaction reverts. If `liquidityData` is empty (`"0x"`): * Pool is created with zero liquidity. * `deposit0` and `deposit1` will both be zero. If valid liquidity data is provided: * Liquidity is added atomically. * `deposit0` and `deposit1` will reflect the deposited token amounts. * Native token value (if supplied) must be consumed by the liquidity call or the transaction will revert. --- # Solidity Example ```solidity PoolCreationDetails memory details = PoolCreationDetails({ poolType: address(poolType), fee: 300, // 3% token0: token0, token1: token1, poolHook: address(0), poolParams: abi.encode(/* pool-specific params */) }); (bytes32 poolId,,) = amm.createPool( details, "", "", "", "" // no initial liquidity ); ``` --- # TypeScript Example (viem-style) ```ts const details = { poolType: poolTypeAddress, fee: 300, token0, token1, poolHook: zeroAddress, poolParams: encodedParams }; const { request } = await publicClient.simulateContract({ address: ammAddress, abi: ammAbi, functionName: "createPool", args: [ details, "0x", // token0HookData "0x", // token1HookData "0x", // poolHookData "0x" // liquidityData ] }); const hash = await walletClient.writeContract(request); ``` --- # Checking If Pool Already Exists Use pool type: ```solidity bytes32 poolId = poolType.computePoolId(details); ``` Then query core: ```solidity PoolState memory poolState = amm.getPoolState(poolId); bool initialized = poolState.token0 != address(0); ``` If state is initialized → pool exists. There is no pool enumeration in core. Index `PoolCreated` events offchain. --- # PoolCreated Event ```solidity event PoolCreated( address indexed poolType, address indexed token0, address indexed token1, bytes32 poolId, address poolHook ); ``` --- # Common Failure Cases Pool creation will revert if: * `token0 == token1` * Fee > 10000 and not sentinel * Sentinel used without pool hook * Pool with same `poolId` already exists * Token hook validation fails * Pool hook validation fails * Custom pool type rejects parameters * `liquidityData` selector does not match `addLiquidity` * `addLiquidity` call reverts * Native token value supplied but unused All failures revert atomically. --- # Design Considerations * `poolHook` is immutable after creation. * `poolType` is immutable. * LP fee configuration is encoded into `poolId`. * Offchain simulation is strongly recommended before submission. * Pool existence must be indexed offchain via events. # Execute Swaps This guide shows how to execute swaps using: - `singleSwap` (one pool) - `multiSwap` (multiple pools) It assumes: - You already understand swap semantics. - You are interacting with pools that already exist. - The executor (`msg.sender`) is directly supplying input tokens. - No custom transfer handler is used (`transferData = "0x"`). For deeper semantics, see: - Protocol Fundamentals → Swaps - Fees & Economics - Hooks --- # Entry Points ## singleSwap ```solidity function singleSwap( SwapOrder calldata swapOrder, bytes32 poolId, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData calldata swapHooksExtraData, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ```` ## multiSwap ```solidity function multiSwap( SwapOrder calldata swapOrder, bytes32[] calldata poolIds, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData[] calldata swapHooksExtraDatas, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` Return values: * `amountIn` — total input collected (including execution-level fees) * `amountOut` — output delivered to `swapOrder.recipient` --- # Constructing SwapOrder ```solidity struct SwapOrder { uint256 deadline; address recipient; int256 amountSpecified; // >0 swap-by-input, <0 swap-by-output uint256 minAmountSpecified; // partial fill tolerance uint256 limitAmount; // min output (input mode) or max input (output mode) address tokenIn; address tokenOut; } ``` You are responsible for: * Setting `deadline` * Encoding swap mode via the sign of `amountSpecified` * Setting `limitAmount` correctly for your mode * Setting `minAmountSpecified` according to your partial-fill policy --- # Execution-Level Fees Swaps accept two fee structs: ```solidity struct BPSFeeWithRecipient { address recipient; uint256 BPS; } struct FlatFeeWithRecipient { address recipient; uint256 amount; } ``` Rules: * If `recipient == address(0)` → fee value must be 0. * BPS bounds depend on swap mode (see Fees & Economics). * Fee-on-top constraints depend on swap mode. --- # Hook Calldata ```solidity struct SwapHooksExtraData { bytes tokenInHook; bytes tokenOutHook; bytes poolHook; bytes poolType; } ``` If no hook requires calldata: ```solidity SwapHooksExtraData({ tokenInHook: "", tokenOutHook: "", poolHook: "", poolType: "" }) ``` For `multiSwap`, provide one `SwapHooksExtraData` per hop. --- # Token Approvals For direct executor settlement: * `msg.sender` supplies `tokenIn` * LBAMM pulls via `transferFrom` * The executor must approve LBAMM beforehand If using wrapped native input: * `msg.value` may be used * Incorrect or unused native value reverts * Excess native value is refunded --- # singleSwap Example (Solidity) ```solidity SwapOrder memory order = SwapOrder({ deadline: block.timestamp + 60, recipient: recipient, amountSpecified: int256(amountIn), // positive => swap-by-input minAmountSpecified: amountIn, limitAmount: minAmountOut, tokenIn: tokenIn, tokenOut: tokenOut }); BPSFeeWithRecipient memory exchangeFee = BPSFeeWithRecipient({ recipient: exchangeFeeRecipient, BPS: 30 }); FlatFeeWithRecipient memory feeOnTop = FlatFeeWithRecipient({ recipient: executorFeeRecipient, amount: executorFee }); SwapHooksExtraData memory hookData = SwapHooksExtraData({ tokenInHook: "", tokenOutHook: "", poolHook: "", poolType: "" }); (uint256 totalIn, uint256 totalOut) = amm.singleSwap{ value: nativeAmountIn }( order, poolId, exchangeFee, feeOnTop, hookData, "" // no transfer handler ); ``` --- # multiSwap Example (Solidity) ```solidity SwapHooksExtraData[] memory hookDatas = new SwapHooksExtraData[](poolIds.length); for (uint256 i = 0; i < poolIds.length; i++) { hookDatas[i] = SwapHooksExtraData("", "", "", ""); } (uint256 totalIn, uint256 totalOut) = amm.multiSwap{ value: msg.value }( order, poolIds, exchangeFee, feeOnTop, hookDatas, "" ); ``` Notes: * `swapHooksExtraDatas.length` must equal `poolIds.length`. * A `Swap` event is emitted per hop. --- # TypeScript Example (singleSwap) ```ts const swapOrder = { deadline: BigInt(Math.floor(Date.now() / 1000) + 60), recipient, amountSpecified: amountIn, // >0 => swap-by-input minAmountSpecified: amountIn, limitAmount: minAmountOut, tokenIn, tokenOut, }; const exchangeFee = { recipient: exchangeFeeRecipient, BPS: 30n, }; const feeOnTop = { recipient: executorFeeRecipient, amount: executorFee, }; const hookData = { tokenInHook: "0x", tokenOutHook: "0x", poolHook: "0x", poolType: "0x", }; const { request } = await publicClient.simulateContract({ address: ammAddress, abi: ammAbi, functionName: "singleSwap", args: [swapOrder, poolId, exchangeFee, feeOnTop, hookData, "0x"], value: msgValue, }); await walletClient.writeContract(request); ``` --- # Common Integration Errors Most swap failures come from: * Expired `deadline` * Incorrect `limitAmount` for swap mode * Fee configuration invalid (recipient zero with non-zero value) * Insufficient allowance or balance * Route array length mismatch (multiSwap) * Hook validation reverts # Using LBAMM with Foundry This guide shows how to install and import LBAMM repositories in a Foundry project. You can install: - `lbamm-core` (interfaces and core logic) - `lbamm-hooks-and-handlers` (standard hook registry and transfer handlers) - One or more pool-type repositories: - `lbamm-pool-type-fixed` - `lbamm-pool-type-single-provider` - `amm-pool-type-dynamic` Install exactly the repositories your project needs. --- ## Installing repositories ### Core only If you only need core interfaces or shared types: ```bash forge install limitbreakinc/lbamm-core ```` --- ### Hooks and handlers If you need the Creator Hook Settings Registry, AMM standard hook, or transfer handlers: ```bash forge install limitbreakinc/lbamm-hooks-and-handlers ``` **Note:** `lbamm-hooks-and-handlers` repository includes `lbamm-core` as a submodule. --- ### Pool types If you are building against a specific pool implementation: ```bash forge install limitbreakinc/lbamm-pool-type-fixed ``` or ```bash forge install limitbreakinc/lbamm-pool-type-single-provider ``` or ```bash forge install limitbreakinc/amm-pool-type-dynamic ``` **Note:** Pool type repositories include `lbamm-hooks-and-handlers` as a submodule. --- ## Remappings You must configure remappings so that `@limitbreak/*` imports resolve correctly. There are two valid patterns depending on how you installed dependencies. --- ### Pattern A — Using nested dependencies from a pool-type repo If you only installed a pool-type repository and want to use its nested `hooks-and-handlers` and `lbamm-core`, point remappings at the nested paths. Example for `lbamm-pool-type-fixed`: ```txt @limitbreak/lbamm-pool-type-fixed/=lib/lbamm-pool-type-fixed/ @limitbreak/lbamm-hooks-and-handlers/=lib/lbamm-pool-type-fixed/lib/lbamm-hooks-and-handlers/ @limitbreak/lbamm-core/=lib/lbamm-pool-type-fixed/lib/lbamm-hooks-and-handlers/lib/lbamm-core/ ``` If using a different pool type, replace the top-level directory accordingly. --- ### Pattern B — Installing repos directly at the top level If you installed `lbamm-core` and/or `lbamm-hooks-and-handlers` directly into `lib/`, use shallow remappings: ```txt @limitbreak/lbamm-core/=lib/lbamm-core/ @limitbreak/lbamm-hooks-and-handlers/=lib/lbamm-hooks-and-handlers/ @limitbreak/lbamm-pool-type-fixed/=lib/lbamm-pool-type-fixed/ @limitbreak/lbamm-pool-type-single-provider/=lib/lbamm-pool-type-single-provider/ @limitbreak/amm-pool-type-dynamic/=lib/amm-pool-type-dynamic/ ``` Only include the entries for the repositories you installed. --- ## Example imports Once remappings are configured, imports use stable namespace paths. ### Import a core hook interface ```solidity import { ILimitBreakAMMTokenHook } from "@limitbreak/lbamm-core/src/interfaces/hooks/ILimitBreakAMMTokenHook.sol"; ``` ### Import a transfer handler ```solidity import { PermitTransferHandler } from "@limitbreak/lbamm-hooks-and-handlers/src/handlers/permit/PermitTransferHandler.sol"; ``` --- ## Version pinning For reproducible builds, install a specific tag or commit: ```bash forge install limitbreakinc/lbamm-pool-type-fixed@ ``` # Integration Guides This section walks through integrating with LBAMM in the order a real application encounters it. It is written primarily for: - **Frontend and DEX UI developers** - **Token issuers deploying their own markets** - Teams building production-facing swap interfaces or routing layers These guides assume you understand core concepts such as: - Pools - Liquidity - Swaps - Hooks - Fee surfaces If you need deeper protocol semantics, refer to: - Protocol Fundamentals - Hooks - Pools - Fees & Economics This section focuses on **how to use the system correctly in production**. --- # The Integration Lifecycle Interacting with LBAMM follows a natural progression: 1. **Create a Pool** Define a market between two tokens and configure pool type and fee behavior. 2. **Add Liquidity** Supply capital to the pool so swaps can execute. 3. **Execute Swaps** Perform single-hop or multi-hop swaps with explicit execution-level fee configuration. 4. **Route Across Pools** Construct multi-hop routes and reason about per-hop and execution-level fee behavior. 5. **Use Transfer Handlers & Signed Orders** Integrate advanced settlement patterns (permit-based flows, CLOB-style flows, etc.). 6. **Read Execution Context** Surface contextual data to hooks, indexers, analytics, and UI components. 7. **Avoid Common Integration Mistakes** Prevent subtle execution, fee, and routing errors. Each guide builds on the previous one. If you are building a production swap interface, you should follow them in order. --- # What These Guides Emphasize These guides prioritize: - Deterministic behavior - Correct fee wiring - Safe parameter bounds - Execution ordering clarity - Production-grade assumptions They do not: - Recommend fee values - Prescribe economic strategy - Provide UI/UX design guidance - Replace formal protocol reference documentation --- # Production Assumptions Examples in this section assume: - Tokens are already deployed. - The LBAMM contracts are deployed. - You are interacting with production contracts. - You are not relying on local-only test scaffolding. We will not cover token deployment, faucet flows, or test token minting. --- # How to Use This Section If you are: ### A DEX UI developer Start with: → Create a Pool (if your interface allows pool creation) → Add Liquidity → Execute Swaps ### A token issuer launching a market Start with: → Create a Pool → Attach Hooks (if applicable) → Add Liquidity ### Building a routing layer Focus on: → Execute Swaps → Route Across Pools → Transfer Handlers --- # Design Philosophy LBAMM is explicit: - Fees are explicit. - Routing is explicit. - Hooks are explicit. - Execution context is explicit. Your integration must respect that explicitness. These guides show how to wire everything correctly without accidentally introducing hidden assumptions. # Manage Liquidity This guide shows how to: - Add liquidity to a pool (`addLiquidity`) - Remove liquidity (`removeLiquidity`) - Collect fees (`collectFees`) It is written for production integrations that already have: - A `poolId` - Token addresses - Token approvals in place This guide intentionally treats `poolParams` as an opaque ABI-encoded blob. The exact encoding is pool-type-specific. > For simplicity, this guide assumes `liquidityHook = address(0)` (no position hook). Position hooks are covered separately. --- ## Overview of liquidity entrypoints LBAMM exposes three liquidity entrypoints: ```solidity function addLiquidity( LiquidityModificationParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external payable returns (uint256 deposit0, uint256 deposit1, uint256 fees0, uint256 fees1); function removeLiquidity( LiquidityModificationParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external returns (uint256 withdraw0, uint256 withdraw1, uint256 fees0, uint256 fees1); function collectFees( LiquidityCollectFeesParams calldata liquidityParams, LiquidityHooksExtraData calldata liquidityHooksExtraData ) external returns (uint256 fees0, uint256 fees1); ```` --- ## Liquidity parameter structs ### Hook calldata bundle Liquidity operations accept a single struct containing optional hook calldata: ```solidity struct LiquidityHooksExtraData { bytes token0Hook; bytes token1Hook; bytes liquidityHook; bytes poolHook; } ``` In the simplest integration: * Provide `"0x"` for all hook calldata fields. * If a token or pool hook requires calldata, supply it in the corresponding field. --- ### Add / Remove liquidity params ```solidity struct LiquidityModificationParams { address liquidityHook; bytes32 poolId; uint256 minLiquidityAmount0; uint256 minLiquidityAmount1; uint256 maxLiquidityAmount0; uint256 maxLiquidityAmount1; uint256 maxHookFee0; uint256 maxHookFee1; bytes poolParams; } ``` Key fields: * **liquidityHook**: position hook address (use `address(0)` for none). * **poolId**: pool identifier. * **min/maxLiquidityAmount0/1**: bounds for the operation (deposit/withdraw semantics are pool-type dependent). * **maxHookFee0/1**: maximum hook fees tolerated (if hooks are enabled). * **poolParams**: ABI-encoded pool-type-specific parameters. > The meaning of the min/max liquidity amount fields depends on pool type (and on whether you are depositing or withdrawing). This guide focuses on wiring and call construction; pool-type semantics are documented in the Pool Types section. --- ### Collect fees params ```solidity struct LiquidityCollectFeesParams { address liquidityHook; bytes32 poolId; uint256 maxHookFee0; uint256 maxHookFee1; bytes poolParams; } ``` --- ## Step 1: Approvals For ERC-20 pools, LBAMM core pulls required tokens from the **provider** (`msg.sender`) via `transferFrom`. That means: * The provider must approve LBAMM for token0 and/or token1 before calling `addLiquidity`. * If the provider is a contract (router), the router must hold tokens and provide approvals (and becomes the provider identity). ### Native value support (wrapped native pools) If one of the pool tokens is the Limit Break wrapped native token: * `msg.value` may be used as part of the liquidity deposit. Rules: * If `msg.value > 0` and **neither token is wrapped native**, the call reverts. * If `msg.value` is provided but is **insufficient** to cover the required native-side deposit, the call reverts. * If `msg.value` is **excess**, it is refunded when native value is used. --- ## Step 2: Add liquidity (no position hook) ### Solidity example ```solidity LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: address(0), // no position hook poolId: poolId, // Pool-type-dependent bounds (placeholder values) minLiquidityAmount0: 0, minLiquidityAmount1: 0, maxLiquidityAmount0: type(uint256).max, maxLiquidityAmount1: type(uint256).max, // Tolerate no hook fees (or set higher if hooks are expected) maxHookFee0: 0, maxHookFee1: 0, // Pool-type-specific encoding (placeholder) poolParams: abi.encode(/* pool-type params */) }); LiquidityHooksExtraData memory hookData = LiquidityHooksExtraData({ token0Hook: "", token1Hook: "", liquidityHook: "", poolHook: "" }); (uint256 deposit0, uint256 deposit1, uint256 fees0, uint256 fees1) = amm.addLiquidity{ value: nativeDepositAmount }(params, hookData); ``` ### TypeScript example (viem-style) ```ts const params = { liquidityHook: zeroAddress, poolId, // Pool-type-dependent bounds (placeholders) minLiquidityAmount0: 0n, minLiquidityAmount1: 0n, maxLiquidityAmount0: (2n ** 256n) - 1n, maxLiquidityAmount1: (2n ** 256n) - 1n, maxHookFee0: 0n, maxHookFee1: 0n, // Pool-type-specific encoding (placeholder) poolParams: "0x" as `0x${string}`, }; const hookData = { token0Hook: "0x", token1Hook: "0x", liquidityHook: "0x", poolHook: "0x", }; const { request } = await publicClient.simulateContract({ address: ammAddress, abi: ammAbi, functionName: "addLiquidity", args: [params, hookData], value: msgValue, // 0n unless wrapped native is involved }); const hash = await walletClient.writeContract(request); ``` --- ## Step 3: Remove liquidity (no position hook) Removing liquidity uses the same `LiquidityModificationParams` struct. ### Solidity example ```solidity LiquidityModificationParams memory params = LiquidityModificationParams({ liquidityHook: address(0), poolId: poolId, // Pool-type-dependent bounds (placeholder values) minLiquidityAmount0: 0, minLiquidityAmount1: 0, maxLiquidityAmount0: type(uint256).max, maxLiquidityAmount1: type(uint256).max, maxHookFee0: 0, maxHookFee1: 0, poolParams: abi.encode(/* pool-type params */) }); LiquidityHooksExtraData memory hookData = LiquidityHooksExtraData({ token0Hook: "", token1Hook: "", liquidityHook: "", poolHook: "" }); (uint256 withdraw0, uint256 withdraw1, uint256 fees0, uint256 fees1) = amm.removeLiquidity(params, hookData); ``` --- ## Step 4: Collect fees (no position hook) Fee collection uses `LiquidityCollectFeesParams`. ### Solidity example ```solidity LiquidityCollectFeesParams memory params = LiquidityCollectFeesParams({ liquidityHook: address(0), poolId: poolId, maxHookFee0: 0, maxHookFee1: 0, poolParams: abi.encode(/* pool-type params */) }); LiquidityHooksExtraData memory hookData = LiquidityHooksExtraData({ token0Hook: "", token1Hook: "", liquidityHook: "", poolHook: "" }); (uint256 fees0, uint256 fees1) = amm.collectFees(params, hookData); ``` --- ## Notes on failed token transfers (debt) Liquidity operations attempt required token transfers as part of execution. If a token transfer fails, LBAMM may store the amount as **debt** rather than reverting. This behavior is intended to make integrations more robust to non-standard token behavior. (Exact debt accounting behavior is covered in the protocol reference sections.) # Submit Signed Orders This guide shows how to: - Accept a maker’s signed permit (EIP-712, PermitC) - Execute it using `PermitTransferHandler` - Submit the order via `directSwap` This flow assumes: - **Fill-or-kill permit** - No cosigner - No custom hook validation - No fee on top - Exchange fee included For handler internals and full permit semantics, see: - Reference → PermitTransferHandler - Reference → directSwap --- # Mental Model In this flow: - Maker signs a PermitC permit authorizing transfer of `tokenIn`. - Executor (taker) submits `directSwap`. - AMM: - Pulls `tokenIn` from maker via PermitTransferHandler. - Pulls `tokenOut` from executor. - Transfers `tokenOut` to maker. - Transfers `tokenIn` to executor. `directSwap` does not compute pricing. All amounts must be supplied explicitly. --- # directSwap Entry Point ```solidity function directSwap( SwapOrder calldata swapOrder, DirectSwapParams calldata directSwapParams, BPSFeeWithRecipient calldata exchangeFee, FlatFeeWithRecipient calldata feeOnTop, SwapHooksExtraData calldata swapHooksExtraData, bytes calldata transferData ) external payable returns (uint256 amountIn, uint256 amountOut); ``` --- # Construct SwapOrder ```solidity SwapOrder memory order = SwapOrder({ deadline: deadline, recipient: maker, amountSpecified: int256(amountIn), // >0 => swap-by-input minAmountSpecified: 0, // not checked in directSwap limitAmount: minAmountOut, // validated by permit tokenIn: tokenIn, tokenOut: tokenOut }); ``` Notes: * Mode is determined by sign of `amountSpecified`. * `minAmountSpecified` is not enforced in `directSwap`. * `limitAmount` is part of the permit signature to ensure minimum amount out to permit signer. --- # Construct DirectSwapParams `directSwap` requires the executor to supply the opposite side explicitly. Mapping rule: * If `amountSpecified > 0` → `swapAmount = amountOut` * If `amountSpecified < 0` → `swapAmount = amountIn` For swap-by-input example: ```solidity DirectSwapParams memory params = DirectSwapParams({ swapAmount: amountOut, // executor provides tokenOut maxAmountOut: maxExecutorPay, // cap executor payment (after fees) minAmountIn: minExecutorReceive // minimum executor receives (after fees) }); ``` Definitions: * `swapAmount` — opposing side of the transaction from `SwapOrder.amountSpecified`, if `amountSpecified` is positive for swap by input then `swapAmount` represents the transaction output amount, if `amountSpecified` is negative for swap by output then `swapAmount` represents the transaction input amount. * `maxAmountOut` — maximum executor payment (post-fee). * `minAmountIn` — minimum executor receipt (post-fee). --- # Include Exchange Fee ```solidity BPSFeeWithRecipient memory exchangeFee = BPSFeeWithRecipient({ recipient: exchangeFeeRecipient, BPS: 30 }); FlatFeeWithRecipient memory feeOnTop = FlatFeeWithRecipient({ recipient: address(0), amount: 0 }); ``` * If recipient is zero, fee must be zero. * Fee ordering semantics are documented in Fees & Economics. --- ## Create Permit (Raw EIP-712 with `Swap` additional data) PermitTransferHandler fill-or-kill permits use an EIP-712 payload of the form: ```text PermitTransferFromWithAdditionalData( uint256 tokenType, address token, uint256 id, uint256 amount, uint256 nonce, address operator, uint256 expiration, uint256 masterNonce, Swap swapData ) Swap( bool partialFill, address recipient, int256 amountSpecified, uint256 limitAmount, address tokenOut, address exchangeFeeRecipient, uint16 exchangeFeeBPS, address cosigner, address hook ) ``` ### What must be bound by the maker signature For the integration flow in this guide (fill-or-kill, no cosigner, no hook): * `swapData.partialFill = false` * `swapData.recipient = maker` * `swapData.amountSpecified` = `SwapOrder.amountSpecified` * `swapData.limitAmount` = `SwapOrder.limitAmount` * `swapData.tokenOut` = `SwapOrder.tokenOut` * `swapData.exchangeFeeRecipient` + `swapData.exchangeFeeBPS` must match the `exchangeFee` passed to `directSwap` * `swapData.cosigner = address(0)` * `swapData.hook = address(0)` ### TypeScript example: EIP-712 typed data to sign This is a minimal example showing the nested structs. You must use the PermitC instance as the EIP-712 verifying contract. ```ts const domain = { name: "PermitC", version: "1", chainId: executionChainId, verifyingContract: permitProcessor, // canonical PermitC instance }; const types = { Swap: [ { name: "partialFill", type: "bool" }, { name: "recipient", type: "address" }, { name: "amountSpecified", type: "int256" }, { name: "limitAmount", type: "uint256" }, { name: "tokenOut", type: "address" }, { name: "exchangeFeeRecipient", type: "address" }, { name: "exchangeFeeBPS", type: "uint16" }, { name: "cosigner", type: "address" }, { name: "hook", type: "address" }, ], PermitTransferFromWithAdditionalData: [ { name: "tokenType", type: "uint256" }, { name: "token", type: "address" }, { name: "id", type: "uint256" }, { name: "amount", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "operator", type: "address" }, { name: "expiration", type: "uint256" }, { name: "masterNonce", type: "uint256" }, { name: "swapData", type: "Swap" }, ], }; const message = { 20, // tokenType ERC20 token: tokenIn, 0, // tokenId is 0 for ERC20 amount: amountIn, nonce, operator: permitTransferHandler, expiration: permitExpiration, masterNonce, swapData: { partialFill: false, recipient: maker, amountSpecified: amountSpecified, // int256 (match SwapOrder.amountSpecified) limitAmount: limitAmount, // match SwapOrder.limitAmount tokenOut: tokenOut, // match SwapOrder.tokenOut exchangeFeeRecipient, exchangeFeeBPS, // uint16 cosigner: zeroAddress, hook: zeroAddress, }, }; const signature = await wallet.signTypedData({ domain, types, primaryType: "PermitTransferFromWithAdditionalData", message, }); ``` --- # Encode transferData Solidity example: ```solidity bytes memory transferData = bytes.concat( abi.encode(permitTransferHandler), // first 32 bytes bytes1(0), // discriminator (fill-or-kill) abi.encode(permitStruct) // ABI-encoded permit ); ``` For full helper implementations, see PermitTransferHandler reference. --- # Call directSwap ```solidity SwapHooksExtraData memory hookData = SwapHooksExtraData({ tokenInHook: "", tokenOutHook: "", poolHook: "", poolType: "" }); (uint256 amountIn, uint256 amountOut) = amm.directSwap( order, params, exchangeFee, feeOnTop, hookData, transferData ); ``` Return values: * `amountIn` — total `tokenIn` moved (including fees) * `amountOut` — total `tokenOut` moved --- # Executor Requirements Executor must: * Hold sufficient `tokenOut` * Approve LBAMM for `tokenOut` * Ensure signed permit matches swap parameters * Ensure `recipient == permit.from` Common failures: * Signature invalid * Permit expired * Nonce reused * Exchange fee mismatch (if bound in signed intent) * Executor payment exceeds `maxAmountOut` # Contract Registry Official LBAMM contract deployments. All contracts are deployed to the same address across supported chains. For infrastructure and deployment details, see: https://developers.apptokens.com/infrastructure --- ## Core | Contract | Address | |----------|----------| | **LBAMM Core** | `NOT_DEPLOYED` | --- ## Pool Types ### Fixed Pool | Contract | Address | |----------|----------| | **FixedPoolType** | `NOT_DEPLOYED` | | **FixedPoolQuoter** | `NOT_DEPLOYED` | ### Single Provider Pool | Contract | Address | |----------|----------| | **SingleProviderPoolType** | `NOT_DEPLOYED` | ### Dynamic Pool | Contract | Address | |----------|----------| | **DynamicPoolType** | `NOT_DEPLOYED` | | **DynamicPoolQuoter** | `NOT_DEPLOYED` | --- ## Transfer Handlers | Contract | Address | |----------|----------| | **PermitTransferHandler** | `NOT_DEPLOYED` | | **CLOBTransferHandler** | `NOT_DEPLOYED` | | **CLOBQuoter** | `NOT_DEPLOYED` | --- ## Hooks | Contract | Address | |----------|----------| | **CreatorHookSettingsRegistery** | `NOT_DEPLOYED` | | **AMMStandardHook** | `NOT_DEPLOYED` | # Core Interfaces Canonical LBAMM interfaces are published in the following repositories. --- ## LBAMM Core Repository: https://github.com/limitbreakinc/lbamm-core - [`ILimitBreakAMM`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMM.sol) Primary interface for LBAMM Core. Defines all external entrypoints (swaps, liquidity, flashloans, pool creation, token settings). - [`ILimitBreakAMMPoolType`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMPoolType.sol) Interface that all custom pool types must implement. - [`ILimitBreakAMMTransferHandler`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMTransferHandler.sol) Interface that all custom transfer handlers must implement. - [`ILimitBreakAMMFlashloanCallback`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/ILimitBreakAMMFlashloanCallback.sol) Interface that flashloan executor contracts must implement. - [`ILimitBreakAMMLiquidityHook`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/hooks/ILimitBreakAMMLiquidityHook.sol) Interface that liquidity position hooks must implement. - [`ILimitBreakAMMPoolHook`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/hooks/ILimitBreakAMMPoolHook.sol) Interface that pool hooks must implement. - [`ILimitBreakAMMTokenHook`](https://github.com/limitbreakinc/lbamm-core/blob/main/src/interfaces/hooks/ILimitBreakAMMTokenHook.sol) Interface that token hooks must implement. --- ## Limit Break Hooks & Transfer Handlers Repository: https://github.com/limitbreakinc/lbamm-hooks-and-handlers - [`ICLOBHook`](https://github.com/limitbreakinc/lbamm-hooks-and-handlers/blob/main/src/handlers/clob/interfaces/ICLOBHook.sol) Interface for order book group key hooks used for maker and taker validation. - [`ITransferHandlerExecutorValidation`](https://github.com/limitbreakinc/lbamm-hooks-and-handlers/blob/main/src/handlers/interfaces/ITransferHandlerExecutorValidation.sol) Interface for executor validation hooks used by PermitTransferHandler. - [`ICreatorHookSettingsRegistry`](https://github.com/limitbreakinc/lbamm-hooks-and-handlers/blob/main/src/hooks/interfaces/ICreatorHookSettingsRegistry.sol) Interface for the Creator Hook Settings Registry used to configure AMM Standard Hook behavior. --- ## Fixed Pool Type Repository: https://github.com/limitbreakinc/lbamm-pool-type-fixed (All pool-type-specific interfaces and implementation live in this repository.) --- ## Single Provider Pool Type Repository: https://github.com/limitbreakinc/lbamm-pool-type-single-provider - [`ISingleProviderPoolHook`](https://github.com/limitbreakinc/lbamm-pool-type-single-provider/blob/main/src/interfaces/ISingleProviderPoolHook.sol) Extension of `ILimitBreakAMMPoolHook` that must be implemented by single provider pool hooks to provide pricing logic and liquidity ownership semantics. --- ## Dynamic Pool Type Repository: https://github.com/limitbreakinc/amm-pool-type-dynamic (All pool-type-specific interfaces and implementation live in this repository.) # Errors This page documents custom errors defined by the LBAMM Core contract. Pool types, hooks, and transfer handlers may define additional custom errors. Integrators should review those modules directly when handling revert cases. --- ## General - `LBAMM__ArrayLengthMismatch()` Array parameters had mismatched lengths. - `LBAMM__BytesLengthExceedsMax()` Bytes length adjustment exceeded the maximum supported size ( > 2^32-1 ). - `LBAMM__CallerMustBeSelf()` A self-only function was called by an address other than the AMM itself. - `LBAMM__Overflow()` An arithmetic overflow occurred. - `LBAMM__Underflow()` An arithmetic underflow occurred. --- ## Pool Creation - `LBAMM__CannotPairIdenticalTokens()` Pool creation attempted with `token0 == token1`. - `LBAMM__InvalidPoolId()` The provided `poolId` failed validation. - `LBAMM__InvalidPoolType()` The provided pool type address has an insufficient number of leading zero bytes. - `LBAMM__LiquidityDataDoesNotCallAddLiquidity()` Pool creation `liquidityData` did not encode an `addLiquidity` call. - `LBAMM__PoolAlreadyExists()` A pool with the same deterministic `poolId` is already initialized. - `LBAMM__PoolCreationWithLiquidityDidNotAddLiquidity()` Pool creation included liquidity data, but no liquidity was actually added. - `LBAMM__PoolCreationWithNoCodeAtToken()` Pool creation attempted where one of the token addresses had no code. - `LBAMM__PoolFeeMustBeLessThan100Percent()` Pool fee exceeded the maximum ( > 10,000 BPS ). --- ## Swaps - `LBAMM__ActualAmountCannotExceedInitialAmount()` Pool type returned an actual input/output exceeding the initially specified amount. - `LBAMM__CannotPartialFillAfterFirstHop()` Multi-hop swap partially filled after the first hop. - `LBAMM__DeadlineExpired()` Swap deadline has passed. - `LBAMM__InvalidExtraDataArrayLength()` Extra data array length was invalid (e.g., per-hop hook data length mismatch). - `LBAMM__InvalidPoolFeeBPS()` Pool fee BPS was invalid - exceeds 10,000 BPS for swap by input or 9,999 BPS for swap by output. - `LBAMM__InvalidPoolFeeHook()` Dynamic fee sentinel was used without a valid pool fee hook (or hook invalid). - `LBAMM__InvalidPoolId()` The provided `poolId`(s) do not exist or resulted in a final input/output token that does not match the swap order. - `LBAMM__LimitAmountExceeded()` Output swap required more input than `limitAmount` allows. - `LBAMM__LimitAmountNotMet()` Input swap produced less output than `limitAmount` requires. - `LBAMM__NoPoolsProvidedForMultiswap()` Multi-hop swap was called with an empty route. - `LBAMM__PartialFillLessThanFees()` A partial fill (output-mode) was smaller than hook fees required. - `LBAMM__PartialFillLessThanMinimumSpecified()` Actual filled amount was less than `minAmountSpecified`. - `LBAMM__PoolHookDataNotSupported()` Pool hook data was provided but not supported for direct swaps. - `LBAMM__RecipientCannotBeAddressZero()` Swap recipient was `address(0)`. --- ## Liquidity - `LBAMM__ExcessiveHookFees()` Total hook fees exceeded `maxHookFee0` and/or `maxHookFee1`. - `LBAMM__ExcessiveLiquidityChange()` Liquidity modification exceeded the configured maximum bounds. - `LBAMM__InsufficientLiquidityChange()` Liquidity modification did not meet the configured minimum bounds. - `LBAMM__PoolDoesNotExist()` A referenced pool does not exist when managing liquidity. --- ## Fees - `LBAMM__ExchangeFeeTransferFailed()` Transfer of exchange fee to recipient failed. - `LBAMM__FeeAmountExceedsInputAmount()` Fee-on-top amount exceeded the swap input amount (input-mode). - `LBAMM__FeeAmountExceedsMaxFee()` Exchange Fee BPS exceeded the maximum allowed for the swap mode. - `LBAMM__FeeOnTopTransferFailed()` Transfer of fee-on-top to recipient failed. - `LBAMM__FeeRecipientCannotBeAddressZero()` Fee recipient was `address(0)` while a non-zero fee was specified. - `LBAMM__InsufficientInputForFees()` Input amount was insufficient to cover required fees. - `LBAMM__InsufficientOutputForFees()` Output amount was insufficient to cover required fees. - `LBAMM__InsufficientProtocolFee()` Pool type returned an invalid / insufficient protocol fee. - `LBAMM__ProtocolFeeTransferFailed()` Transfer of protocol fees failed. --- ## Flashloans - `LBAMM__CannotCollectFeesDuringFlashloan()` Fee collection was attempted during a flashloan operation. - `LBAMM__FlashloansDisabled()` Flashloans are disabled (fee BPS configured above allowed max). - `LBAMM__FeeTokenNotAllowedForFlashloan()` Flashloan fee token was not allowed for the loan token. - `LBAMM__FlashloanExecutionFailed()` Flashloan callback did not return the expected value. - `LBAMM__FlashloanFeeTokenCannotBeAddressZero()` Flashloan fee token was the zero address. - `LBAMM__InvalidFlashloanBPS()` Flashloan fee BPS was invalid (exceeds supported range). --- ## Transfer & Settlement - `LBAMM__InputNotWrappedNative()` Native value was provided but the input token was not wrapped native. - `LBAMM__InsufficientValue()` `msg.value` was insufficient for required wrapped-native payment. - `LBAMM__InvalidTransferHandler()` Transfer handler calldata was provided but did not decode to a valid address. - `LBAMM__RefundFailed()` Refund transfer (excess native value) failed. - `LBAMM__TokenInTransferFailed()` Transfer of `tokenIn` failed. - `LBAMM__TokenOutTransferFailed()` Transfer of `tokenOut` failed. - `LBAMM__TokenOwedTransferFailed()` Transfer of a token owed to a user failed. - `LBAMM__TransferHookFeeTransferFailed()` Transfer of a hook fee failed. - `LBAMM__ValueNotUsed()` `msg.value` was provided but not used. --- ## Hooks & Token Settings - `LBAMM__HookFlagsMissingRequiredFlags()` Enabled hook flags did not include all required flags. - `LBAMM__UnsupportedHookFlags()` One or more enabled hook flags were not supported by the hook, or when clearing the token hook a non-zero `packedSettings` was supplied. # Events This page documents events emitted by the LBAMM Core contract. Pool types, hooks, and transfer handlers may emit additional events. Integrators should review the documentation and source code of those modules to determine any additional indexing requirements. --- ## Pool Lifecycle ### `PoolCreated` ```solidity event PoolCreated( address indexed poolType, address indexed token0, address indexed token1, bytes32 poolId, address poolHook ); ``` Emitted when a new pool is created. --- ## Token Settings ### `TokenSettingsUpdated` ```solidity event TokenSettingsUpdated( address indexed token, address indexed tokenHook, uint32 packedSettings ); ``` Emitted when a token’s hook configuration or settings are updated. --- ## Swaps ### `Swap` ```solidity event Swap( bytes32 indexed poolId, address indexed recipient, bool zeroForOne, uint256 amountIn, uint256 amountOut, uint256 lpFeeAmount ); ``` Emitted when a pool swap executes. Note: * Emitted once per hop during multi-hop swaps. * Does not include execution-level fees (exchange fee / fee-on-top). --- ### `DirectSwap` ```solidity event DirectSwap( address indexed executor, address indexed recipient, address indexed tokenIn, address tokenOut, uint256 amountIn, uint256 amountOut ); ``` Emitted when a `directSwap` executes. --- ## Liquidity ### `LiquidityAdded` ```solidity event LiquidityAdded( bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1 ); ``` Emitted when liquidity is added to a pool. --- ### `LiquidityRemoved` ```solidity event LiquidityRemoved( bytes32 indexed poolId, address indexed provider, uint256 amount0, uint256 amount1, uint256 fees0, uint256 fees1 ); ``` Emitted when liquidity is removed from a pool. --- ### `FeesCollected` ```solidity event FeesCollected( bytes32 indexed poolId, address indexed provider, uint256 fees0, uint256 fees1 ); ``` Emitted when accumulated LP fees are collected. --- ## Flashloans ### `Flashloan` ```solidity event Flashloan( address indexed requester, address indexed executor, address indexed loanedToken, uint256 loanAmount, address feeToken, uint256 feeAmount ); ``` Emitted when a flashloan is executed. --- ## Hook Fee Claims ### `TokensClaimed` ```solidity event TokensClaimed( address indexed recipient, address indexed token, uint256 amount ); ``` Emitted when hook-managed token balances are claimed. --- ## Protocol Fees ### `ProtocolFeeTaken` ```solidity event ProtocolFeeTaken( address indexed token, uint256 amount ); ``` Emitted when protocol fees are accrued. --- ### `ProtocolFeesCollected` ```solidity event ProtocolFeesCollected( address indexed protocolFeeReceiver, address indexed token, uint256 protocolFeesCollected ); ``` Emitted when protocol fees are withdrawn. --- ### `ProtocolFeesSet` ```solidity event ProtocolFeesSet( uint16 lpFeeBPS, uint16 exchangeFeeBPS, uint16 feeOnTopBPS ); ``` Emitted when global protocol fee configuration is updated. --- ### `FlashloanFeeSet` ```solidity event FlashloanFeeSet(uint16 flashLoanBPS); ``` Emitted when the flashloan fee configuration is updated. --- ### `TokenFeeSet` ```solidity event TokenFeeSet( address indexed token, uint16 hopFeeBPS ); ``` Emitted when a token hop fee is configured. --- ### `ExchangeProtocolFeeOverrideSet` ```solidity event ExchangeProtocolFeeOverrideSet( address recipient, bool feeOverrideEnabled, uint16 protocolFeeBPS ); ``` Emitted when an exchange fee protocol override is configured for a recipient. --- ### `FeeOnTopProtocolFeeOverrideSet` ```solidity event FeeOnTopProtocolFeeOverrideSet( address recipient, bool feeOverrideEnabled, uint16 protocolFeeBPS ); ``` Emitted when a fee-on-top protocol override is configured for a recipient. --- ### `LPProtocolFeeOverrideSet` ```solidity event LPProtocolFeeOverrideSet( bytes32 poolId, bool feeOverrideEnabled, uint16 protocolFeeBPS ); ``` Emitted when an LP fee protocol override is configured for a pool. # Reference This section documents the canonical interfaces and behavioral specifications of LBAMM contracts. Pages in this section describe: - Public functions - Struct definitions - Events - Configuration constraints For integration workflows and examples, see **Integration Guides**. # Fundamentals This page defines what an **execution** is in LBAMM, how execution context is constructed, and what guarantees hooks, pools, and integrators can rely on. It builds directly on the canonical vocabulary defined in [Concepts & Terminology](./intro/concepts-terminology). Everything else in the protocol—hooks, pools, routing semantics, and integrations—builds on this execution model. --- ## What is an execution? An **execution** is a single top-level call into LBAMM core via one of its external entrypoints, such as: * Pool creation * Swaps (single-hop, multi-hop, or direct) * Liquidity addition or removal * Fee collection * Flash loans * Token settings updates by issuers An execution: * Begins at the start of the external call to LBAMM * Ends when that call completes (successfully or via revert) * May span **multiple pools and tokens** * Shares a **single execution context** throughout its lifetime There is exactly **one active execution context** at a time. --- ## Execution context The **execution context** is the shared, non-spoofable state constructed by LBAMM core that describes who is acting and how an operation is being performed. It is: - Created at the beginning of the external LBAMM call - Destroyed at the end of that call - Immutable for the duration of the execution (except for explicitly scoped, protocol-defined fields) All hooks, pool types, and internal protocol logic observe the same execution context. ### Canonical actors LBAMM deliberately keeps the set of actors small and explicit. * **Executor** * The `msg.sender` calling into LBAMM * The primary identity hooks should rely on * Always present and reliable * **Recipient** (optional) * An address specified by certain actions (e.g. swaps) * May differ from the executor * Included so hooks can allow or restrict specific execution–recipient combinations > **Non-actor:** `tx.origin` is intentionally irrelevant and must not be used for authorization or logic. --- ## Routing and hops LBAMM itself is the **first-class router**. There is no protocol-level router abstraction. If third parties introduce routing contracts, they are simply executors calling into LBAMM, and hooks may restrict or block execution based on executor identity. ### Multi-hop swaps For multi-hop swaps: * A single execution may traverse multiple pools * The output of one hop becomes the input of the next * The execution context is shared across all hops LBAMM exposes a hop index in execution context with the following properties: - Zero-based - Increments monotonically for each hop within a multi-hop swap This allows hooks to reason about where in a multi-hop route an interaction occurs without introducing an external router abstraction. --- ## Nested operations Some protocol flows are *logically nested* but do **not** create new executions. Examples include: * Pool creation that immediately adds initial liquidity * Swaps or liquidity modifications that trigger fee collection These operations: * Occur within the same execution context * Do not reset or replace execution metadata * Must obey all execution guarantees ### Deferred fee collection Hooks may request fee collection during execution. Such requests are: * **Queued** during execution * **Settled after** the primary operation completes For example: 1. A hook on the second hop of a three-hop swap requests fee collection 2. The second and third hops complete 3. Input tokens are collected from the executor 4. Output tokens are sent to the recipient 5. The queued fee request is settled This ensures fee settlement observes the finalized execution outcome while remaining bound to the same execution context. --- ## Direct swaps A **direct swap** is an execution with **no pool interaction**. * Tokens are exchanged directly between two parties * No AMM pricing logic is involved * A transfer handler is typically used Despite the absence of pools: * Token hooks fire exactly as they do in pooled swaps * Execution context semantics are identical --- ## Flash loans Flash loans use the same execution model and context as all other LBAMM operations. During a flash loan: * Token hooks fire to authorize the loan * If the loaned token specifies an alternate fee currency, hooks validate that the fee currency is permitted Repayment and fee settlement occur within the same execution context and are subject to the same revert semantics. --- ## Failure semantics Execution-level failure rules are intentionally simple: * If any hook reverts, **the entire execution reverts** * Partial effects are not committed across execution boundaries ### Partial fills Some pool types may support bounded partial fills: * For example, a partial fill on the first hop of a swap * Always constrained by user-specified parameters (e.g. minimum output) Partial fills do not weaken execution atomicity; they are an explicit outcome of pool logic. --- ## What the execution model guarantees You can rely on the following invariants: * Executor identity is reliable and non-spoofable * Execution context is shared across all pools and tokens in an execution * Hooks observe consistent execution metadata * Execution context persists across token transfers triggered by the execution ## What it does not guarantee * The existence of routers or routing abstractions * That executors are permissionless across all hooks * That all pool types behave identically These distinctions are intentional and enable policy to be expressed explicitly rather than implicitly. --- ## Why this matters By making execution explicit and first-class, LBAMM enables: * Token-level enforcement across venues * Reliable hook behavior without guesswork * Composable pool and liquidity designs * Clear security boundaries for integrators and hook authors Subsequent sections build directly on these guarantees. # Security Model This page documents the security model, trust boundaries, and delegated risk surfaces of LBAMM. It is intended for: - Smart contract auditors - Teams building custom pool types - Teams deploying hooks - Integrators implementing transfer handlers or signed-order flows - Security researchers LBAMM is intentionally extensible. Security posture depends not only on core correctness, but on how extension surfaces are implemented and configured. This page focuses on: - What the core guarantees - What extension modules are responsible for - What must always remain true - Where risk is intentionally delegated It does **not** restate pool math or hook APIs. --- # Architecture & Security Model LBAMM core is: - Invariant-agnostic - Module-driven (pool types define swap and liquidity math) - Hook-extensible (token, position, pool) - Transfer-handler extensible (custom settlement flows) - Fee-surface configurable Core enforces structural and accounting constraints. Execution logic is delegated to extension modules. Security review must account for: - Core contract logic - All enabled pool type modules - All enabled hook contracts - All enabled transfer handlers - Permit infrastructure (if used) → See: Protocol Fundamentals → See: Hooks → See: Fees & Economics --- # Trust Boundaries ## Core Contract The core contract: - Enforces poolId structure and fee caps - Enforces reserve and fee balance accounting - Enforces swap and liquidity limit constraints - Holds all pool token balances - Executes hooks in defined order - Delegates invariant logic to pool types Core does not: - Implement pricing logic - Validate offchain orderbooks - Sandbox extension modules --- ## Pool Type Modules Pool types: - Define invariant math and price movement - Define liquidity accounting semantics - Must preserve reserve and fee balance correctness Incorrect pool logic can break economic assumptions while remaining structurally valid at the core level. --- ## Hooks Hooks: - May validate, revert, or impose additional constraints - May accrue hook fees - Execute inside the core call graph Token hooks apply globally to a token across pools. Hooks cannot bypass core accounting constraints, but can influence execution flow and fee behavior. --- ## Transfer Handlers Transfer handlers: - Implement settlement mechanics - May validate signatures and bind swap intent - Must correctly report and transfer token amounts - Must not violate fee or limit constraints Mis-implementation can result in incorrect settlement or signature misuse. --- # Critical Invariants The following properties must hold across all pool types and extensions: - Withdrawals and swaps cannot exceed pool reserve balances - Fee collection cannot exceed fee balances - Swap limit constraints must be enforced - Liquidity limit constraints must be enforced - Execution-level fee caps must be respected Invariant enforcement is distributed between: - Core (structural + accounting guarantees) - Pool types (invariant correctness) --- # Fee Surfaces & Configuration Risk LBAMM exposes multiple fee surfaces: - LP fees (pool-defined) - Exchange fees - Fee-on-top - Protocol fee overrides - Hook-managed fees - Flashloan fees Security review must consider: - Mode-dependent fee caps - Recipient binding correctness - Interaction between hook fees and maxHookFee bounds - Multi-hop aggregation behavior Misconfiguration risks include: - Excessive hook fees - Incorrect recipient configuration - Governance-level fee overrides --- # Extension Risk Model LBAMM intentionally exposes programmable extension points: - Custom pool types - Custom hooks - Custom transfer handlers These modules: - Execute within the core call graph - May revert execution - May influence settlement flow - May influence fee attribution LBAMM does not sandbox extensions. Extension contracts must be reviewed with the same rigor as core. Security posture depends on: - Correct configuration - Restrictive governance - Careful review of all enabled modules --- # Audit Scope Guidance Third-party audits should include: - Core contract - All enabled pool types - All enabled hooks - All enabled transfer handlers - Fee configuration paths - Token settings and role management Audits should also consider: - Signature binding in transfer handlers - Governance permissions and upgrade surfaces - Fee override configuration paths Out-of-scope risks may include: - Offchain orderbook matching logic - Frontend enforcement assumptions - Non-canonical PermitC deployments --- # Audit Reports The following independent security audits have been performed: - **[Auditor Name] – Audit Title – Date** - Scope: Core / Pool Types / Hooks / etc. - Report: - **[Auditor Name] – Audit Title – Date** - Scope: … - Report: Where applicable: - Findings were addressed prior to mainnet deployment. - Remaining issues (if any) are documented in the linked reports. If you are conducting an independent review and require clarification, please contact: **contact**.