diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3dcad3f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Scroll + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 5e5190e..e7c7be3 100644 --- a/README.md +++ b/README.md @@ -2,63 +2,88 @@ [![test](https://github.com/scroll-tech/canvas-contracts/actions/workflows/contracts.yml/badge.svg)](https://github.com/scroll-tech/canvas-contracts/actions/workflows/contracts.yml) -![Components overview](images/overview.png "Overview") +## Welcome to Scroll Canvas -([Editable link](https://viewer.diagrams.net/?tags=%7B%7D&highlight=0000ff&edit=_blank&layers=1&nav=1&title=skelly-v4.drawio#R7VpLc6M4EP41rpo5xIWEMeYYx8nsIdma2Rx2clRABs0K5BVybO%2BvXwkknk7ijCEwNalUyqjVenV%2F%2FVDDxL6K91842kR3LMB0Aq1gP7FXEwgBsIH8UZRDTllY85wQchJoppJwT%2F7Dmmhp6pYEOK0xCsaoIJs60WdJgn1RoyHO2a7Otma0vuoGhbhFuPcRbVP%2FJoGI9Ckcq6T%2FgUkYmZWBpXtiZJg1IY1QwHYVkn09sa84YyJ%2FivdXmCrhGbnk426e6S02xnEiThkQf%2Ft2uwvggibu7Q9%2Fk1hfCbzQykjFwRwYB%2FL8usm4iFjIEkSvS%2BqSs20SYDWrJVslzy1jG0kEkvgDC3HQykRbwSQpEjHVvXLD%2FPBdj88aD6oxdUxzta92rg66lQrO%2FinUIAW4XBNKrxhlPNu7vV742PcLzkrP48KZOWqOttS0IFO25T5%2BQVQGfYiHWLzAZ%2Bd8So6VBbROvmAWY3kmycAxRYI81XGGNFzDgq%2FUqHzQSn2DgsEsn%2FgJ0a1eagLnVB5gGZCnmurn%2F24VFpd%2BLrZLtcfw8ZPjTaBc2yp%2FP2dylLaUiIs1igk95Nx3OKEsZ7qTgPD1s5wZxRIZSz3%2FlZQ0wVz2%2FIl3zc58SMwSlm6Q0kexUprBSa0DrM0%2B76AkwReRtr%2Bsy9Fd82K%2Beah%2BkRA4FZ8%2Bm8NLYWbnz3uPWsEtepTOrIZcREmYyGdfokeewF4%2BYS6I9BaXuiMmQZAbCZb7RY%2FZfAp3G0YSkSnXWU6clcKuPJQxNKsNy5fMVa2K95Mj%2Fk%2BvWHMxNdTpURfW1Fo4bj72UJvpZFzqyb%2Bqk5WzXHj1EWy9TqW9NHFcbOkMaLeQfX1539LkLiIC3%2BdYWu1kkGpoNN3kYWNN9sqn1dRyxMeCZzTwrKTthVWTsu3q9q4STjQpqkQSQ%2BvcI8CW2O79CMfIWMYjN1bxFw5JapY8R6Ytd7zM%2FrS0j9F7EDx0Bha8O2isLRqnxVq8J%2BJ7ySlbD5WecpBq1OPzm9T5atS1T4y6cFRR12uperCkqR5mOrXFzpQ3G5XyzNVjDNobNuU9VX%2FOqPRnHwlwnFG6RGp%2FR6JcyuiTykdHljk4zsgyh%2FZVoibYDsXXsaM6XeZuU%2BZwYJk77ybzLiDbFN9saPEB%2BJv48ld9NOjaRzeufxoB0G0gADRUmwcdPaqHS2Hb%2B3%2FUO37Zeocx304KHrAGTN06E%2B5FsmiqH4v6DD1WPxYfQFe%2F1pRnZQrMxwT202Ppm5Hs2I26Rn%2BVonbForfgCU8tSIBqOaIoTrxfQQLOhwq4512KwFEz6L36FFyql3CymbAE55QborauFYS4MBw%2BRWlKfEPWbKBnPfadEIFZw1y9hrnmQGolRFIq6FBh0z7mzevc%2FBy%2FfMh30GnQMkKvBK3LLFFRUaN5Mf%2FK2f782vMorpWwKeyha9HwI3vIMEbSdIuzu%2FSYsodXUuXOUom5Z00tS6JzMfechV2%2FuM1U38yZyX%2FHdW3YV2CyPpBYXtiWh7YfXGGKQxmCWDImjPYIS5DBEngAQMe1JDgXdec596ae58zmnu1CD3p9AXPgb2PKetJDte%2B59PiVTCtAaZTtC5yVNRk38Ktlv27LyUh7Q340Dst5EX%2Fn25M1BZb51uNns9n%2BCxoGWhUdyQxwTej4ytzNTwtm3sDpnH3sK68hndfkTZXxM%2FzRqa8oDbpedUhlvdC27XpKZGp9I7ah9usin2Mk2iY0Ij9nsNtFjRdA68zCrinkNt9b9Ki09iuLwvH19klW1y4RgndzibJZfjWdK6H89ty%2B%2Fh8%3D)) +We are thrilled to have you join us in building unique discoveries with [Scroll Canvas](https://scroll.io/canvas), a new product designed for ecosystem projects to interact with users in a more tailored way. -## ScrollBadge Schema and Resolver +Try Canvas at [scroll.io/canvas](https://scroll.io/canvas) -We define a *Scroll badge* [EAS schema](https://docs.attest.sh/docs/core--concepts/schemas): +## Overview -``` -address badge -bytes payload +**Scroll Canvas** allows users to showcase on-chain credentials, status, and achievements called **Badges** issued and collected across the Scroll ecosystem. +Users can mint a non-transferable and unique personal persona to collect and display their **Badges**. + +### Key Features + +- **Canvas**: Each Canvas is a smart contract minted through the `ProfileRegistry` contract by the user on Scroll’s website. +- **Badges**: Attestations of achievements and traits verified through the [Ethereum Attestation Service](https://docs.attest.sh/docs/welcome) (EAS), issued by different projects and the Scroll Foundation. + Badges are wallet-bound and non-transferable. + +Differences between attestations and NFTs: + +| Attestation | NFT | +| --- | --- | +| Witness Proofs | Tokenized Assets | +| Non-transferable | Transferable | +| Recorded on disk (blockchain history) | Recorded in memory (blockchain states) | +| Prove ownership at a point in time | Exercise custodianship of an asset | + +## Developer Quickstart + +Visit the [Developer Documentation](./docs) in this repo to learn more about Canvas. + +See [Deployments](./docs/deployments.md) for the official Canvas contract addresses. + +See the [Integration Guide](https://scrollzkp.notion.site/Introducing-Scroll-Canvas-Badge-Integration-Guide-8656463ab63b42e8baf924763ed8c9d5) for more information. + +## Support + +For questions regarding Canvas and custom badge development, please join [Scroll dev support channel](https://discord.com/channels/853955156100907018/1028102371894624337) on Discord. + +## Running the Code + +### Node.js + +First install [`Node.js`](https://nodejs.org/en) and [`npm`](https://www.npmjs.com/). +Run the following command to install [`yarn`](https://classic.yarnpkg.com/en/): + +```bash +npm install --global yarn ``` -This schema is tied to `ScrollBadgeResolver`. -Every time a Scroll badge attestation is created or revoked, `ScrollBadgeResolver` executes some checks. -After that, it forwards the call to the actual badge implementation. +### Foundry -## Profiles +Install `foundryup`, the Foundry toolchain installer: -Each user can create a `Profile` contract, minted through the `ProfileRegistry` contract. -Each wallet can mint only one profile. -All profiles share the same implementation, upgradable by Scroll to enable new features. +```bash +curl -L https://foundry.paradigm.xyz | bash +``` -The main use of profiles is personalization. -Users can configure a username and an avatar. -Users can also decide which badges they atach to their profile, and in which order. +If you do not want to use the redirect, feel free to manually download the `foundryup` installation script from [here](https://raw.githubusercontent.com/foundry-rs/foundry/master/foundryup/foundryup). Then, run `foundryup` in a new terminal session or after reloading `PATH`. -## Badges +Other ways to install Foundry can be found [here](https://github.com/foundry-rs/foundry#installation). -Each badge is an EAS attestation that goes through the `ScrollBadgeResolver` contract and a badge contract. +### Install Dependencies -Each badge type is a standalone contract, inheriting from `ScrollBadge`. -This badge contract can implement arbitrary logic attached to the attestation. -Badges implement a `badgeTokenURI` interface, similar to `ERC721.tokenURI`. +Run the following command to install all dependencies locally. -Badges are minted to the user's wallet address. -The user can express their personalization preferences (attach and order badges, choose a profile photo) through their `Profile`. +``` +yarn +``` + +### Run Contract Tests + +Run the following command to run the contract tests. -See [badges](./docs/badges.md) for details. +``` +yarn test +``` -### Extensions +## Contributing -This repo contains some useful [extensions](src/badge/extensions): -- `ScrollBadgeAccessControl` restricts who can create and revoke this badge. -- `ScrollBadgeCustomPayload` adds custom payload support to the badge. -- `ScrollBadgeDefaultURI` sets a default badge token URI. -- `ScrollBadgeEligibilityCheck` adds a standard on-chain eligibility check interface. -- `ScrollBadgeNoExpiry` disables expiration for the badge. -- `ScrollBadgeNonRevocable` disables revocation for the badge. -- `ScrollBadgeSBT` attaches an SBT token to each badge attestation. -- `ScrollBadgeSelfAttest` ensures that only the recipient of the badge can create the badge. -- `ScrollBadgeSingleton` ensures that each user can only have at most one of the badge. +We welcome community contributions to this repository. +For larger changes, please [open an issue](https://github.com/scroll-tech/canvas-contracts/issues/new/choose) and discuss with the team before submitting code changes. -### Examples +## License -This repo also contains some [examples](src/badge/examples): -- `ScrollBadgeSimple` is a simple badge with fixed metadata. -- `ScrollBadgePermissionless` is a permissionless badge that anyone can mint to themselves. -- `ScrollBadgeLevels` is an SBT badge that stores a level in its payload and renders different images based on this level. -- `ScrollBadgeTokenOwner` is a badge that is tied to the ownership of a Scroll Origins NFT. +Scroll Monorepo is licensed under the [MIT](./LICENSE) license. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..ba58f7e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,70 @@ +# Scroll Canvas Developer Documentation + +![Components overview](../images/overview.png "Overview") + +([Editable link](https://viewer.diagrams.net/?tags=%7B%7D&highlight=0000ff&edit=_blank&layers=1&nav=1&title=skelly-v4.drawio#R7VpLc6M4EP41rpo5xIWEMeYYx8nsIdma2Rx2clRABs0K5BVybO%2BvXwkknk7ijCEwNalUyqjVenV%2F%2FVDDxL6K91842kR3LMB0Aq1gP7FXEwgBsIH8UZRDTllY85wQchJoppJwT%2F7Dmmhp6pYEOK0xCsaoIJs60WdJgn1RoyHO2a7Otma0vuoGhbhFuPcRbVP%2FJoGI9Ckcq6T%2FgUkYmZWBpXtiZJg1IY1QwHYVkn09sa84YyJ%2FivdXmCrhGbnk426e6S02xnEiThkQf%2Ft2uwvggibu7Q9%2Fk1hfCbzQykjFwRwYB%2FL8usm4iFjIEkSvS%2BqSs20SYDWrJVslzy1jG0kEkvgDC3HQykRbwSQpEjHVvXLD%2FPBdj88aD6oxdUxzta92rg66lQrO%2FinUIAW4XBNKrxhlPNu7vV742PcLzkrP48KZOWqOttS0IFO25T5%2BQVQGfYiHWLzAZ%2Bd8So6VBbROvmAWY3kmycAxRYI81XGGNFzDgq%2FUqHzQSn2DgsEsn%2FgJ0a1eagLnVB5gGZCnmurn%2F24VFpd%2BLrZLtcfw8ZPjTaBc2yp%2FP2dylLaUiIs1igk95Nx3OKEsZ7qTgPD1s5wZxRIZSz3%2FlZQ0wVz2%2FIl3zc58SMwSlm6Q0kexUprBSa0DrM0%2B76AkwReRtr%2Bsy9Fd82K%2Beah%2BkRA4FZ8%2Bm8NLYWbnz3uPWsEtepTOrIZcREmYyGdfokeewF4%2BYS6I9BaXuiMmQZAbCZb7RY%2FZfAp3G0YSkSnXWU6clcKuPJQxNKsNy5fMVa2K95Mj%2Fk%2BvWHMxNdTpURfW1Fo4bj72UJvpZFzqyb%2Bqk5WzXHj1EWy9TqW9NHFcbOkMaLeQfX1539LkLiIC3%2BdYWu1kkGpoNN3kYWNN9sqn1dRyxMeCZzTwrKTthVWTsu3q9q4STjQpqkQSQ%2BvcI8CW2O79CMfIWMYjN1bxFw5JapY8R6Ytd7zM%2FrS0j9F7EDx0Bha8O2isLRqnxVq8J%2BJ7ySlbD5WecpBq1OPzm9T5atS1T4y6cFRR12uperCkqR5mOrXFzpQ3G5XyzNVjDNobNuU9VX%2FOqPRnHwlwnFG6RGp%2FR6JcyuiTykdHljk4zsgyh%2FZVoibYDsXXsaM6XeZuU%2BZwYJk77ybzLiDbFN9saPEB%2BJv48ld9NOjaRzeufxoB0G0gADRUmwcdPaqHS2Hb%2B3%2FUO37Zeocx304KHrAGTN06E%2B5FsmiqH4v6DD1WPxYfQFe%2F1pRnZQrMxwT202Ppm5Hs2I26Rn%2BVonbForfgCU8tSIBqOaIoTrxfQQLOhwq4512KwFEz6L36FFyql3CymbAE55QborauFYS4MBw%2BRWlKfEPWbKBnPfadEIFZw1y9hrnmQGolRFIq6FBh0z7mzevc%2FBy%2FfMh30GnQMkKvBK3LLFFRUaN5Mf%2FK2f782vMorpWwKeyha9HwI3vIMEbSdIuzu%2FSYsodXUuXOUom5Z00tS6JzMfechV2%2FuM1U38yZyX%2FHdW3YV2CyPpBYXtiWh7YfXGGKQxmCWDImjPYIS5DBEngAQMe1JDgXdec596ae58zmnu1CD3p9AXPgb2PKetJDte%2B59PiVTCtAaZTtC5yVNRk38Ktlv27LyUh7Q340Dst5EX%2Fn25M1BZb51uNns9n%2BCxoGWhUdyQxwTej4ytzNTwtm3sDpnH3sK68hndfkTZXxM%2FzRqa8oDbpedUhlvdC27XpKZGp9I7ah9usin2Mk2iY0Ij9nsNtFjRdA68zCrinkNt9b9Ki09iuLwvH19klW1y4RgndzibJZfjWdK6H89ty%2B%2Fh8%3D)) + + +# Overview + +Scroll Canvas consists of the following components: +- [**ProfileRegistry**](../src/profile/ProfileRegistry.sol): A contract for users to mint and query their Canvases. +- [**Profile**](../src/profile/Profile.sol): Each Canvas is an instance of the profile smart contract. +- [**EAS**](https://docs.attest.org/docs/welcome): A technology for issuing on-chain attestations. +- [**ScrollBadgeResolver**](../src/resolver/ScrollBadgeResolver.sol): Each attestation passes through this resolver before the badge is minted. It enforces Canvas badge rules. +- [**ScrollBadge**](../src/badge/ScrollBadge.sol): Each badge is a contract the conforms to a certain [interface](../src/interfaces/IScrollBadge.sol). + + +## Profiles + +Each user can mint a [`Profile`](../src/profile/Profile.sol) instance through [`ProfileRegistry`](../src/profile/ProfileRegistry.sol). +This contract is the user's Canvas, and minting it is a prerequisite to collecting badges. +Each wallet can only mint one profile. +All profiles share the same implementation, upgradable by Scroll to enable new features. + +The main use of profiles is personalization. +Users can configure a username and an avatar. +Users can also decide which badges they attach to their profile, and in which order they want to display them. + +See the [Canvas Interaction Guide](./canvas-interaction-guide.md) section for more details. + + +## ScrollBadge Schema and Resolver + +We define a *Scroll badge* [EAS schema](https://docs.attest.org/docs/core--concepts/schemas): + +``` +address badge +bytes payload +``` + +This schema is tied to `ScrollBadgeResolver`. +Every time a Scroll badge attestation is created or revoked through EAS, `ScrollBadgeResolver` executes some checks and actions. +After this, it forwards the call to the actual badge implementation. + +You can find the schema UID in the [Deployments](./deployments.md) section. +Browse the Scroll mainnet badge attestations on the [EAS Explorer](https://scroll.easscan.org/schema/view/0xd57de4f41c3d3cc855eadef68f98c0d4edd22d57161d96b7c06d2f4336cc3b49). + + +## Badges + +Each badge is an [EAS attestation](https://docs.attest.org/docs/core--concepts/attestations) that goes through the [`ScrollBadgeResolver`](../src/resolver/ScrollBadgeResolver.sol) contract and a badge contract. + +Each badge type is a standalone contract that inherits from [`ScrollBadge`](../src/badge/ScrollBadge.sol). +This badge contract can implement arbitrary logic attached to the attestation. +Badges implement a `badgeTokenURI` interface, similar to `ERC721.tokenURI`. + +Badges are minted to the user's wallet address. +The user can express their personalization preferences (attach and reorder badges, choose a profile photo) through their Canvas [`Profile`](../src/profile/Profile.sol). + +See the [Badges](./badges.md) section for more details, and [Badge Examples](./badge-examples.md) for Solidity code examples. + + +## Explore the Documentation + +Explore the following pages to learn more about different aspects of Canvas: +- [Deployments](./deployments.md) lists the official Canvas contract addresses on Scroll mainnet and on the Scroll Sepolia testnet. +- [Badges](./badges.md) introduces the basic requirements for badge contracts and lists resources for getting started as a badge developer. +- [Badge Examples](./badge-examples.md) shows the process of developing custom badges by going through some common examples and use cases. +- [Canvas Interaction Guide](./canvas-interaction-guide.md) lists common questions and examples for interacting with Canvas profiles and badges. +- [Official Badges](./official-badges) contains addresses and documentation for some badges issued by Scroll. diff --git a/docs/badge-examples.md b/docs/badge-examples.md new file mode 100644 index 0000000..cf6f976 --- /dev/null +++ b/docs/badge-examples.md @@ -0,0 +1,310 @@ +# Badge Examples + +- [Permissionless Singleton Badge](#permissionless-singleton-badge) + - [Writing the badge from scratch](#writing-the-badge-from-scratch) + - [Reusing extensions](#reusing-extensions) + - [Minting the badge](#minting-the-badge) +- [Custom Payload and Complex On-Chain Eligibility Checks](#custom-payload-and-complex-on-chain-eligibility-checks) +- [Backend-Authorized Badges](#backend-authorized-badges) + +## Permissionless Singleton Badge + +### Writing the badge from scratch + +First, we will walk through an example of implementing a simple badge from scratch. +The example here, `MyScrollBadge`, is a permissionless badge, i.e. anyone can mint it independently. +The only restriction is that we will require that each user mint for themselves, i.e. you cannot gift a badge to someone else. +We will also ensure that it is a singleton badge, meaning that each user can mint at most one badge. + +We start by importing [`Attestation`](https://github.com/ethereum-attestation-service/eas-contracts/blob/b84f18326432e5f23ec0dfa5dab06ea154c2a502/contracts/Common.sol#L25) from EAS, and [`ScrollBadge`](../src/badge/ScrollBadge.sol) from Canvas. +Just like our example `MyScrollBadge` here, each valid badge is a direct or indirect subclass of `ScrollBadge`. +This ensures that each badge implements the correct interface that [`ScrollBadgeResolver`](../src/resolver/ScrollBadgeResolver.sol) knows how to interact with. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Attestation} from "@eas/contracts/IEAS.sol"; +import {ScrollBadge} from "../ScrollBadge.sol"; + +contract MyScrollBadge is ScrollBadge { + // ... +} +``` + +For correct display, each badge must have a [badge token URI](./badges.md#badge-token-uri). +In this example, we will use a static token URI that is shared for all badges minted with this contract. +This can be a link to a JSON stored on a centralized backend, or stored on decentralized storage like IPFS. + +It is important to note that each badge must configure the correct resolved address during deployment. +See the address in [Deployments](./deployments.md). + +```solidity +string public staticTokenURI; + +constructor(address resolver_, string memory tokenURI_) ScrollBadge(resolver_) { + staticTokenURI = tokenURI_; +} + +function badgeTokenURI(bytes32 /*uid*/ ) public pure override returns (string memory) { + return staticTokenURI; +} +``` + +Next, we implement the `onIssueBadge` hook that is called when your badge is minted. +You can execute checks and revert or return false to prevent an invalid badge from being minted. +Here, we implement two checks: +First, we make sure that the user does not already have a badge. +Second, we check whether the user is minting for themselves or not. + +```solidity +function onIssueBadge(Attestation calldata attestation) internal virtual override returns (bool) { + if (!super.onIssueBadge(attestation)) { + return false; + } + + // singleton + if (hasBadge(attestation.recipient)) { + revert SingletonBadge(); + } + + // self-attest + if (attestation.recipient != attestation.attester) { + revert Unauthorized(); + } + + return true; +} +``` + +Similarly, we also need to implement the `onRevokeBadge` hook, but in most cases, this will be empty. + +```solidity +/// @inheritdoc ScrollBadge +function onRevokeBadge(Attestation calldata attestation) internal virtual override returns (bool) { + return super.onRevokeBadge(attestation); +} +``` + +Finally, we add the on-chain eligibility check function `isEligible` so that the frontend can check if the user is eligible or not. + +```solidity +function isEligible(address recipient) external virtual returns (bool) { + return !hasBadge(recipient); +} +``` + +And now we are ready! +You have implemented your first badge. + +### Reusing extensions + +This type of badge is quite common, so we offer some useful extensions that you can reuse. +You can write the same contract using the [`ScrollBadgeSelfAttest`](../src/badge/extensions/ScrollBadgeSelfAttest.sol), [`ScrollBadgeEligibilityCheck`](../src/badge/extensions/ScrollBadgeEligibilityCheck.sol), and [`ScrollBadgeSingleton`](../src/badge/extensions/ScrollBadgeSingleton.sol) extensions from this repo. + +```solidity +pragma solidity 0.8.19; + +import {Attestation} from "@eas/contracts/IEAS.sol"; + +import {ScrollBadge} from "../ScrollBadge.sol"; +import {ScrollBadgeSelfAttest} from "../extensions/ScrollBadgeSelfAttest.sol"; +import {ScrollBadgeEligibilityCheck} from "../extensions/ScrollBadgeEligibilityCheck.sol"; +import {ScrollBadgeSingleton} from "../extensions/ScrollBadgeSingleton.sol"; + +/// @title ScrollBadgePermissionless +/// @notice A simple badge that anyone can mint in a permissionless manner. +contract ScrollBadgePermissionless is ScrollBadgeSelfAttest, ScrollBadgeEligibilityCheck, ScrollBadgeSingleton { + string public staticTokenURI; + + constructor(address resolver_, string memory tokenURI_) ScrollBadge(resolver_) { + staticTokenURI = tokenURI_; + } + + /// @inheritdoc ScrollBadge + function onIssueBadge(Attestation calldata attestation) + internal + virtual + override (ScrollBadge, ScrollBadgeSelfAttest, ScrollBadgeSingleton) + returns (bool) + { + return super.onIssueBadge(attestation); + } + + /// @inheritdoc ScrollBadge + function onRevokeBadge(Attestation calldata attestation) + internal + virtual + override (ScrollBadge, ScrollBadgeSelfAttest, ScrollBadgeSingleton) + returns (bool) + { + return super.onRevokeBadge(attestation); + } + + /// @inheritdoc ScrollBadge + function badgeTokenURI(bytes32 /*uid*/ ) public pure override returns (string memory) { + return staticTokenURI; + } +} +``` + +### Minting the badge + +Permissionless badges can be minted directly through EAS. +The user can simply call [`EAS.attest`](https://github.com/ethereum-attestation-service/eas-contracts/blob/b84f18326432e5f23ec0dfa5dab06ea154c2a502/contracts/IEAS.sol#L117) and provide the Scroll Canvas [schema UID](./deployments.md) and the attestation. +The attestation payload must include the badge contract address. + + +## Custom Payload and Complex On-Chain Eligibility Checks + +You can attach a custom payload to your badge attestations, that can then be processed in your badge contract. +Let us consider an example of a simple badge that attests that you have reached a certain level. + +Start by deciding your badge payload format. +In this case, we only need a single `uint8` field, signifying the user's level. +Note: The badge payload is encoded using Solidity's [ABI encoding](https://docs.soliditylang.org/en/develop/abi-spec.html). + +```solidity +string constant BADGE_LEVELS_SCHEMA = "uint8 scrollLevel"; + +function decodePayloadData(bytes memory data) pure returns (uint8) { + return abi.decode(data, (uint8)); +} +``` + +If your contract inherits from the [`ScrollBadgeCustomPayload`](../src/badge/extensions/ScrollBadgeCustomPayload.sol) extension, then you can conveniently use the `getPayload` function. + +```solidity +contract ScrollBadgeLevels is ScrollBadgeCustomPayload { + // ... + + /// @inheritdoc ScrollBadgeCustomPayload + function getSchema() public pure override returns (string memory) { + return BADGE_LEVELS_SCHEMA; + } + + function getCurrentLevel(bytes32 uid) public view returns (uint8) { + Attestation memory badge = getAndValidateBadge(uid); + bytes memory payload = getPayload(badge); + (uint8 level) = decodePayloadData(payload); + return level; + } +} +``` + +You can access and interpret the payload during badge minting (in `onIssueBadge`) and badge revocation (in `onRevokeBadge`): + +```solidity +function onIssueBadge(Attestation calldata attestation) internal override returns (bool) { + if (!super.onIssueBadge(attestation)) return false; + + bytes memory payload = getPayload(attestation); + (uint8 level) = decodePayloadData(payload); + + if (level > 10) { + revert InvalidLevel(); + } + + return true; +} +``` + +You can also use the custom payload when constructing the token URI (in `badgeTokenURI`). +This is particularly useful for badges that generate different token URIs based on each badge using [Data URLs](https://developer.mozilla.org/en-US/docs/Web/URI/Schemes/data): + +```solidity +/// @inheritdoc ScrollBadge +function badgeTokenURI(bytes32 uid) public pure override returns (string memory) { + uint8 level = getCurrentLevel(uid); + + string memory name = string(abi.encode("Level #", Strings.toString(level))); + string memory description = "Level Badge"; + string memory image = ""; // IPFS, HTTP, or data URL + string memory issuerName = "Scroll"; + + string memory tokenUriJson = Base64.encode( + abi.encodePacked('{"name":"', name, '", "description":"', description, ', "image": "', image, ', "issuerName": "', issuerName, '"}') + ); + + return string(abi.encodePacked("data:application/json;base64,", tokenUriJson)); +} +``` + +You can see the full example in [`ScrollBadgeLevels`](../src/badge/examples/ScrollBadgeLevels.sol). + + +## Backend-Authorized Badges + +Backend authorized badges are badges that require a signed permit to be minted. +This is generally used for badges when there is a centralized issuer who wishes to control who can and cannot mint. +Another use case is off-chain eligibility check, when the signer of the permit vouches that the user is eligible. + +The simplest backend-authorized badge is implemented like this (see [`ScrollBadgeSimple`](../src/badge/examples/ScrollBadgeSimple.sol)): + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.19; + +import {Attestation} from "@eas/contracts/IEAS.sol"; + +import {ScrollBadgeAccessControl} from "../extensions/ScrollBadgeAccessControl.sol"; +import {ScrollBadgeSingleton} from "../extensions/ScrollBadgeSingleton.sol"; +import {ScrollBadge} from "../ScrollBadge.sol"; + +/// @title ScrollBadgeSimple +/// @notice A simple badge that has the same static metadata for each token. +contract ScrollBadgeSimple is ScrollBadgeAccessControl, ScrollBadgeSingleton { + string public sharedTokenURI; + + constructor(address resolver_, string memory tokenUri_) ScrollBadge(resolver_) { + sharedTokenURI = tokenUri_; + } + + /// @inheritdoc ScrollBadge + function onIssueBadge(Attestation calldata attestation) + internal + override (ScrollBadgeAccessControl, ScrollBadgeSingleton) + returns (bool) + { + return super.onIssueBadge(attestation); + } + + /// @inheritdoc ScrollBadge + function onRevokeBadge(Attestation calldata attestation) + internal + override (ScrollBadgeAccessControl, ScrollBadgeSingleton) + returns (bool) + { + return super.onRevokeBadge(attestation); + } + + /// @inheritdoc ScrollBadge + function badgeTokenURI(bytes32 /*uid*/ ) public view override returns (string memory) { + return sharedTokenURI; + } +} +``` + +Importantly, this badge inherits from `ScrollBadgeAccessControl`. +This allows the deployed to control who is authorized to mint. + +To implement the backend-authorized minting flow, you need to deploy two contracts: the badge contract itself (`ScrollBadgeSimple` in this example) and the attester proxy contract ([`AttesterProxy`](../src/AttesterProxy.sol)). +The attester proxy is a simple contract that verifies permits and mints badges. + +For such badges, all attestations are minted through the attester proxy. +For this reason, you need to authorize the proxy to mint your badge by calling `badge.toggleAttester(attesterProxy, true)`. + +The attester proxy in turn needs to know who is authorized to sign permits, which is typically a private key in your backend. +You also need to authorize this account by calling `attesterProxy.toggleAttester(signer, true)`. + +Finally, you need to configure a backend that implements two public APIs: eligibility check and claim. + +Minting through the Scroll Canvas website then works as follows: +1. The frontend calls your eligibility API to see if the user is eligible. +2. If yes, a mint button is shown to the user. When the user clicks it, the frontend calls your claim API to get the signer permit. +3. The signed permit is submitted from the user's wallet to your attester proxt contract. +4. The attester proxy contract verifies the signature and then creates an attestation through EAS. +5. EAS creates an attestation, then calls `ScrollBadgeResolver`, which in turn calls your badge contract. +6. Your badge contract executes any additional actions and checks. diff --git a/docs/badges.md b/docs/badges.md index a735057..9da7420 100644 --- a/docs/badges.md +++ b/docs/badges.md @@ -1,19 +1,30 @@ -# Canvas Badge FAQ +# Badges + +This section introduces the basic concepts of Canvas badges. +For jumping into code examples, see [Badge Examples](./badge-examples.md). + +- [What is a badge?](#what-is-a-badge) +- [How to implement a new badge?](#how-to-implement-a-new-badge) +- [Badge Token URI](#badge-token-uri) +- [Ways to Issue Badges](#ways-to-issue-badges) +- [Overview of Requirements](#overview-of-requirements) +- [Upgradable Badges](#upgradable-badges) +- [Extensions](#extensions) +- [Examples](#examples) +- [Troubleshooting](#troubleshooting) ### What is a badge? Each Canvas badge is an [EAS attestation](https://docs.attest.sh/docs/core--concepts/attestations), with some additional logic attached to it. -The badge attestation uses the official Scroll Canvas schema (see `SCROLL_SEPOLIA_BADGE_SCHEMA` in [deployments.md](./deployments.md)). -This means that the badge data includes two fields: `address badge, bytes payload`, and badges will go through the official Canvas badge resolver contract. +The badge attestation uses the official Scroll Canvas schema (see `BADGE_SCHEMA` in [Deployments](./deployments.md)). +This means that the badge data contains two fields: `address badge, bytes payload`, and badges are issued through the official Canvas badge [resolver contract](../src/resolver/ScrollBadgeResolver.sol). ### How to implement a new badge? -As a badge developer, you need to deploy a badge contract that inherits from [`ScrollBadge`](../src/badge/ScrollBadge.sol). -Additionally, you can use one of more [extensions](../src/badge/extensions). - -The badge must implement 3 APIs (see [`IScrollBadge`](../src/interfaces/IScrollBadge.sol)): +Each badge must implement a certain interface to ensure it is compatible with Canvas. +In particular, each badge must implement 3 APIs (see [`IScrollBadge`](../src/interfaces/IScrollBadge.sol)): - `issueBadge`: Implement arbitrary logic that is triggered when a new badge is created. - `revokeBadge`: Implement arbitrary logic that is triggered when a badge is revoked. - `badgeTokenURI`: Return the badge token URI. @@ -21,39 +32,277 @@ The badge must implement 3 APIs (see [`IScrollBadge`](../src/interfaces/IScrollB In most cases, the badge contract would use a static image, shared by all instances of this badge. However, on-chain-generated SVG data URLs are also possible. +As a badge developer, it is strongly recommended that your contract inherits from [`ScrollBadge`](../src/badge/ScrollBadge.sol). +Additionally, you can use one or more [extensions](../src/badge/extensions). Refer to the examples in [examples](../src/badge/examples). -> While this is not compulsory, we recommend creating badges that do no expire, are non-revocable, and are singletons (at most 1 badge per user). +> While this is not mandatory, we recommend creating badges that do no expire, are non-revocable, and are singletons (at most 1 badge per user). + + +### Badge Token URI + +Each badge must define a badge token URI. + +The badge token URI is very similar to the tokenURI in ERC-721. +It must point to a metadata JSON object that contains `name`, `description`, `image`, and `issuerName`. +You can use a normal URL, an IPFS link, or a [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs) as your token URI. +The metadata is used by the Canvas frontend to render the badge. + + +For example, the badge token URI https://nft.scroll.io/canvas/year/2024.json points to the following metadata: + +```json +{ + "name": "Ethereum Year", + "description": "Check out the Ethereum Year Badge! It's like a digital trophy that shows off the year your wallet made its debut on Ethereum. It's a little present from Scroll to celebrate all the cool stuff you've done in the Ethereum ecosystem.", + "image": "https://nft.scroll.io/canvas/year/2024.webp", + "issuerName": "Scroll" +} +``` + +Your badge contract can provide a single URI for all badges, in which case all instances of your badge will look the same. +Alternatively, you can also render a different image for different instances of your badge, see [`EthereumYearBadge`](../src/badge/examples/EthereumYearBadge.sol). +You should also configure a default badge token URI, see [`ScrollBadgeDefaultURI`](../src/badge/extensions/ScrollBadgeDefaultURI.sol). +Design guidelines for badge images: +- Maximum resolution: 480px x 480px +- Optimal resolution: 600px x 600px +- File size: Under 300KB -### How to mint a badge? -Badges are created by attesting to the recipient using the `SCROLL_SEPOLIA_BADGE_SCHEMA`. +### Ways to Issue Badges + +Badges are created by attesting to the recipient using the [`BADGE_SCHEMA`](./deployments.md). EAS provides multiple interfaces to attest: `attest`, `attestByDelegation`, `multiAttest`, `multiAttestByDelegation`. See [`IEAS.sol`](https://github.com/ethereum-attestation-service/eas-contracts/blob/master/contracts/IEAS.sol). -Another useful example is [`AttesterProxy.sol`](../src/AttesterProxy.sol), which allows creating unordered delegated attestations. -There are 3 main badge minting flows: -1. **Fully permissionless**. - The user attests to themselves using `EAS.attest`. - The badge contract ensures that the issuer is authorized. +There are three main badge types of badges: + +1. **Permissionless**. + Permissionless badges allow users to attest to themselves using `EAS.attest`. + The badge contract ensures that the user is authorized to mint. See [`ScrollBadgePermissionless.sol`](../src/badge/examples/ScrollBadgePermissionless.sol) and [`ScrollBadgeTokenOwner.sol`](../src/badge/examples/ScrollBadgeTokenOwner.sol). 2. **Backend-authorized**. - A centralized backend implements some off-chain eligibility check. + For backend-authorized badges, the issuer maintains a centralized backend service. + This backend implements some off-chain eligibility check and exposes an eligibility check and claim API. If the user is authorized to mint, the backend issues a signed permit. - The user then mints by calling `AttesterProxy.attestByDelegation`. - Note: In this case, the badge issuer will be the address of `AttesterProxy`. + The user then mints using this permit. + + See [this document](https://scrollzkp.notion.site/Badge-APIs-95890d7ca14944e2a6d34835ceb6b914) for the API requirements. + + For backend-authorized badges, you need to deploy two contracts: the badge contract, and an [`AttesterProxy`](../src/AttesterProxy.sol). + `AttesterProxy` allows executing delegated attestations in arbitrary order. + The user can mint the badge by calling `AttesterProxy.attestByDelegation` and providing the signed permit. + +3. **Gifted**. + Badges can also be issued with no user interaction. + To do this, the issuer uses `EAS.attest` or `EAS.multiAttest` to airdrop these badges to a list of users. + + +### Overview of Requirements + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeDescriptionRequirementsExamples
+ + `Permissionless` + + + + Badge checks eligibility based on smart contract. + + **Example: Badges attesting to completing an onchain transaction or holding an NFT are eligible to mint the badge.** + + + + **Basic Requirements**: + +
    +
  • + + The badge contract is deployed on the **Scroll Mainnet** and verified on [Scrollscan](https://scrollscan.com). + +
  • +
  • + + The badge contract is configured to use the correct **badge resolver address**, see [Deployments](./deployments.md). + +
  • +
  • + + The badge contract has a **default token URI**, see [ScrollBadgeDefaultURI](../src/badge/extensions/ScrollBadgeDefaultURI.sol). + +
  • +
+ + **Additional Requirements**: + +
    +
  • + + The badge contract implements on-chain eligibility check, see [ScrollBadgeEligibilityCheck](../src/badge/extensions/ScrollBadgeEligibilityCheck.sol). + +
  • +
  • + + Your project is listed on [Scroll Ecosystem - Browse all protocols](https://scroll.io/ecosystem#protocols). (If not listed, apply [here](https://tally.so/r/waxLBW).) + +
  • +
+
+ + [`ScrollBadgePermissionless`](../src/badge/examples/ScrollBadgePermissionless.sol), [`ScrollBadgeTokenOwner`](../src/badge/examples/ScrollBadgeTokenOwner.sol), [`ScrollBadgeWhale`](../src/badge/examples/ScrollBadgeWhale.sol). + +
+ + `Backend-authorized` + + + + Badge checks eligibility based on the issuer’s API. + + **Example: Badges attesting to completing offchain actions or a certain allow list.** + + + + All **Basic Requirements**, plus + +
    +
  • + + The [**check API**](https://scrollzkp.notion.site/Badge-APIs-95890d7ca14944e2a6d34835ceb6b914) (off-chain eligibility check) and [**claim API**](https://scrollzkp.notion.site/Badge-APIs-95890d7ca14944e2a6d34835ceb6b914) have been deployed to **production**. + +
  • +
  • + + All URLs mentioned above are configured for cross-origin access (CORS) on https://scroll.io. + +
  • +
  • + + The attester proxy contract is deployed on the **Scroll Mainnet** and verified on [Scrollscan](https://scrollscan.com). + +
  • +
  • + + The attester proxy contract is authorized to mint your badge (through `badge.toggleAttester`). + +
  • +
  • + + The backend signer is authorized to sign permits (through `attesterProxy.toggleAttester`). + +
  • +
+
+ + [`EthereumYearBadge`](../src/badge/examples/EthereumYearBadge.sol), [`ScrollBadgeSimple`](../src/badge/examples/ScrollBadgeSimple.sol). + +
+ + `Gifted` + + + + Badge checks eligibility based on the issuer’s API and automatically sends to users' canvas. There is no minting required for users to display the badge. + + **Example: Badges attesting to ownership or paid membership on other platforms / chains.** + + + + All **Basic Requirements**, plus + +
    +
  • + + The [**check API**](https://scrollzkp.notion.site/Badge-APIs-95890d7ca14944e2a6d34835ceb6b914) has been deployed to **production**. + +
  • +
+
+ N/A +
+ + +### Upgradable Badges + +> This section is not about contract upgradability. +> If you want to make your badge contract upgradable, use any standard [upgradability pattern](https://docs.openzeppelin.com/upgrades-plugins/1.x/proxies). + +Upgradable badges are badges that can evolve over time. +This pattern is most suitable for badges that represent that the user has reached a certain "level". +A user can first mint a badge at a certain level. +Then, once the user is eligible, they can upgrade their badge to a higher level. + +Upgradable badges must implement the [`IScrollBadgeUpgradeable`](../src/badge/extensions/IScrollBadgeUpgradeable.sol) interface. +Currently this interface only supports on-chain upgrade conditions. + + + +### Extensions + +This repo contains some useful [extensions](src/badge/extensions): +- `ScrollBadgeAccessControl` restricts who can create and revoke this badge. +- `ScrollBadgeCustomPayload` adds custom payload support to the badge. +- `ScrollBadgeDefaultURI` sets a default badge token URI. +- `ScrollBadgeEligibilityCheck` adds a standard on-chain eligibility check interface. +- `ScrollBadgeNoExpiry` disables expiration for the badge. +- `ScrollBadgeNonRevocable` disables revocation for the badge. +- `ScrollBadgeSBT` attaches an SBT token to each badge attestation. +- `ScrollBadgeSelfAttest` ensures that only the recipient of the badge can create the badge. +- `ScrollBadgeSingleton` ensures that each user can only have at most one of the badge. + + +### Examples -3. **Airdropped**. - Badges can also be issues with no user interaction. - To do this, the issuer uses `EAS.attest` or `EAS.multiAttest`. +This repo also contains some [examples](src/badge/examples): +- `ScrollBadgeSimple` is a simple badge with fixed metadata. +- `ScrollBadgePermissionless` is a permissionless badge that anyone can mint to themselves. +- `ScrollBadgeLevels` is an SBT badge that stores a level in its payload and renders different images based on this level. +- `ScrollBadgeTokenOwner` is a badge that is tied to the ownership of a Scroll Origins NFT. -### How to ensure that the badge can be shown on scroll.io? +### Troubleshooting -Simply provide the deployed badge contract address to the Scroll team. +We recommend going through the [requirements](#overview-of-requirements) before your badge is published. +If your badge minting transaction reverts, we recommend debugging using `cast`: -### How to ensure that the badge can be minted on scroll.io? +```sh +cast run --rpc-url https://rpc.scroll.io [txhash] +``` -TBA +This call will simulate the transaction in a local environment and show you the call stack and revert reason. diff --git a/docs/integration-faq.md b/docs/canvas-interaction-guide.md similarity index 95% rename from docs/integration-faq.md rename to docs/canvas-interaction-guide.md index ef0f8ae..57d8720 100644 --- a/docs/integration-faq.md +++ b/docs/canvas-interaction-guide.md @@ -1,13 +1,11 @@ -# Canvas Integration FAQ +# Canvas Interaction Guide -In the examples on this page, we use the configurations from [deployments.md](./deployments.md), as well as the following values: +This document will show you the basic steps how one would interact with a Canvas profile. + +In the examples on this page, we use the configurations from [Deployments](./deployments.md), as well as the following values: ```bash # Canvas badges -- each badge type is a new contract, here we only have three simple test contracts -SCROLL_MAINNET_SIMPLE_BADGE_A_ADDRESS="0xB1Dbd079c62d181926E5A54932Bb1b15F760e8A0" -SCROLL_MAINNET_SIMPLE_BADGE_B_ADDRESS="0xe626E631BdDcd985D02D2eEe4fbdF901b52AE33C" -SCROLL_MAINNET_SIMPLE_BADGE_C_ADDRESS="0xe485f8fcBf3b678e83d208fa3f1933a315d58356" - SCROLL_SEPOLIA_SIMPLE_BADGE_A_ADDRESS="0x30C98067517f8ee38e748A3aF63429974103Ea6B" SCROLL_SEPOLIA_SIMPLE_BADGE_B_ADDRESS="0xeBFc9B95328B2Cdb3c4CA8913e329c101d2Abbc2" SCROLL_SEPOLIA_SIMPLE_BADGE_C_ADDRESS="0x64492EF5a60245fbaF65F69782FCf158F3a8e3Aa" @@ -59,7 +57,7 @@ To mint a profile with a referral, produce a signed referral, then submit it alo ### How to list all badges that a user has? -We can use the EAS GraphQL API to query a user's Canvas badges. +We can use the [EAS GraphQL API](https://docs.attest.org/docs/developer-tools/api) to query a user's Canvas badges. > Warning: Badges are minted to the user's wallet address, not to their profile address! diff --git a/docs/ethereum-year-badge.md b/docs/official-badges/ethereum-year-badge.md similarity index 100% rename from docs/ethereum-year-badge.md rename to docs/official-badges/ethereum-year-badge.md diff --git a/docs/scroll-origins-badge.md b/docs/official-badges/scroll-origins-badge.md similarity index 100% rename from docs/scroll-origins-badge.md rename to docs/official-badges/scroll-origins-badge.md