diff --git a/.github/workflows/auto-label-bot.yml b/.github/workflows/auto-label-bot.yml index 77590a1b89f212..d4edd8fdb50bb5 100644 --- a/.github/workflows/auto-label-bot.yml +++ b/.github/workflows/auto-label-bot.yml @@ -1,6 +1,10 @@ on: pull_request_target: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + name: Auto Label Bot jobs: jekyll-label-action: diff --git a/.github/workflows/auto-review-bot.yml b/.github/workflows/auto-review-bot.yml index 61a79630a5738f..e0dd983eea6f07 100644 --- a/.github/workflows/auto-review-bot.yml +++ b/.github/workflows/auto-review-bot.yml @@ -5,6 +5,10 @@ on: types: - completed +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + name: Auto Review Bot jobs: auto-review-bot: @@ -40,7 +44,7 @@ jobs: GITHUB-TOKEN: ${{ secrets.TOKEN }} PR_NUMBER: ${{ steps.save-pr-number.outputs.pr }} CORE_EDITORS: '@lightclient,@axic,@gcolvin,@SamWilsn,@Pandapip1' - ERC_EDITORS: '@lightclient,@axic,@SamWilsn,@Pandapip1' + ERC_EDITORS: '@axic,@SamWilsn,@Pandapip1' NETWORKING_EDITORS: '@lightclient,@axic,@SamWilsn' INTERFACE_EDITORS: '@lightclient,@axic,@SamWilsn,@Pandapip1' META_EDITORS: '@lightclient,@axic,@gcolvin,@SamWilsn,@Pandapip1' diff --git a/.github/workflows/auto-review-trigger.yml b/.github/workflows/auto-review-trigger.yml index 69c081d7bd7737..644e1b01622567 100644 --- a/.github/workflows/auto-review-trigger.yml +++ b/.github/workflows/auto-review-trigger.yml @@ -19,6 +19,10 @@ on: types: - created +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + name: Auto Review Bot Trigger jobs: trigger: diff --git a/.github/workflows/auto-stagnate-bot.yml b/.github/workflows/auto-stagnate-bot.yml index c7e8d73c7d1663..abc25f1acefb0a 100644 --- a/.github/workflows/auto-stagnate-bot.yml +++ b/.github/workflows/auto-stagnate-bot.yml @@ -11,9 +11,9 @@ jobs: name: Auto Stagnant Bot steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@e2f20e631ae6d7dd3b768f56a5d2af784dd54791 - name: Setup Node.js Environment - uses: actions/setup-node@v2 + uses: actions/setup-node@8c91899e586c5b171469028077307d293428b516 with: node-version: '14' - name: auto-stagnant-bot diff --git a/.github/workflows/ci-rerun-trigger.yml b/.github/workflows/ci-rerun-trigger.yml index 31121c2eeccda2..7321bf4e7db1ae 100644 --- a/.github/workflows/ci-rerun-trigger.yml +++ b/.github/workflows/ci-rerun-trigger.yml @@ -10,7 +10,7 @@ jobs: name: Trigger steps: - name: Trigger - uses: actions/github-script@v6 + uses: actions/github-script@d556feaca394842dc55e4734bf3bb9f685482fa0 if: github.event.issue.pull_request && contains(github.event.comment.body, '@eth-bot rerun') with: script: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 662c7b49bff775..e0ae1c0bb0d753 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,10 @@ on: - ready_for_review - edited +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: save-pr: name: Save PR Number @@ -27,7 +31,7 @@ jobs: echo $MERGE_SHA > ./pr/merge_sha - name: Upload PR Number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 with: name: pr_number path: pr/ @@ -39,7 +43,7 @@ jobs: steps: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - + - name: Install OpenSSL run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev @@ -67,17 +71,25 @@ jobs: steps: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - - name: Delete Unchanged Files - uses: Pandapip1/delete-unchanged-files@2c27069573bbeb6703790ac5c872e9b1a100d96c + + - name: Get Changed Files + id: changed + continue-on-error: true + run: | + echo "CHANGED_FILES<> $GITHUB_ENV + gh pr diff ${{ github.event.number }} --name-only | sed -e 's|$|,|' | xargs -i echo "{}" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run CodeSpell uses: codespell-project/actions-codespell@2391250ab05295bddd51e36a8c6295edb6343b0e + if: steps.changed.outcome == 'success' with: check_filenames: true ignore_words_file: config/.codespell-whitelist + path: ${{ env.CHANGED_FILES }} skip: .git,Gemfile.lock,**/*.png,**/*.gif,**/*.jpg,**/*.svg,.codespell-whitelist,vendor,_site,_config.yml,style.css - only_warn: 1 eipw-validator: name: EIP Walidator @@ -87,8 +99,34 @@ jobs: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - uses: ethereum/eipw-action@dist + - uses: ethereum/eipw-action@3b4a2599d62433ef0c99766ab9836b0accf80edd id: eipw with: token: ${{ secrets.GITHUB_TOKEN }} unchecked: 1, 5069 + + markdownlint: + name: Markdown Linter + runs-on: ubuntu-latest + steps: + - name: Checkout EIP Repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Get Changed Files + id: changed + continue-on-error: true + run: | + echo "CHANGED_FILES<> $GITHUB_ENV + gh pr diff ${{ github.event.number }} --name-only | grep -E -x '[^/]+\.md|EIPS/eip-[0-9]+\.md' >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint + uses: DavidAnson/markdownlint-cli2-action@16d9da45919c958a8d1ddccb4bd7028e8848e4f1 + if: steps.changed.outcome == 'success' + with: + command: config + globs: | + config/.markdownlint.yaml + ${{ env.CHANGED_FILES }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ef732f82be6f67..f0eebe2a6c43e0 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,11 +24,13 @@ jobs: close-issue-message: This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback. days-before-issue-stale: 7 days-before-issue-close: 49 # 49 + 7 weeks = 3 months - exempt-issue-labels: discussions-to + exempt-issue-labels: discussions-to, e-consensus stale-issue-label: w-stale # PR config stale-pr-message: There has been no activity on this pull request for 2 weeks. It will be closed after 3 months of inactivity. If you would like to move this PR forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review. close-pr-message: This pull request was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment. days-before-pr-stale: 14 days-before-pr-close: 42 # 42 + 14 weeks = 3 months + exempt-pr-labels: e-review, e-consensus + exempt-pr-milestones: "Manual Merge Queue" stale-pr-label: w-stale diff --git a/EIPS/eip-1.md b/EIPS/eip-1.md index 58c33b6b7bf197..a580d23501dc42 100644 --- a/EIPS/eip-1.md +++ b/EIPS/eip-1.md @@ -38,6 +38,7 @@ An EIP must meet certain minimum criteria. It must be a clear and complete descr ### Special requirements for Core EIPs If a **Core** EIP mentions or proposes changes to the EVM (Ethereum Virtual Machine), it should refer to the instructions by their mnemonics and define the opcodes of those mnemonics at least once. A preferred way is the following: + ``` REVERT (0xfe) ``` @@ -48,13 +49,13 @@ REVERT (0xfe) Parties involved in the process are you, the champion or *EIP author*, the [*EIP editors*](#eip-editors), and the [*Ethereum Core Developers*](https://github.com/ethereum/pm). -Before you begin writing a formal EIP, you should vet your idea. Ask the Ethereum community first if an idea is original to avoid wasting time on something that will be rejected based on prior research. It is thus recommended to open a discussion thread on [the Ethereum Magicians forum](https://ethereum-magicians.org/) to do this. +Before you begin writing a formal EIP, you should vet your idea. Ask the Ethereum community first if an idea is original to avoid wasting time on something that will be rejected based on prior research. It is thus recommended to open a discussion thread on [the Ethereum Magicians forum](https://ethereum-magicians.org/) to do this. Once the idea has been vetted, your next responsibility will be to present (by means of an EIP) the idea to the reviewers and all interested parties, invite editors, developers, and the community to give feedback on the aforementioned channels. You should try and gauge whether the interest in your EIP is commensurate with both the work involved in implementing it and how many parties will have to conform to it. For example, the work required for implementing a Core EIP will be much greater than for an ERC and the EIP will need sufficient interest from the Ethereum client teams. Negative community feedback will be taken into consideration and may prevent your EIP from moving past the Draft stage. ### Core EIPs -For Core EIPs, given that they require client implementations to be considered **Final** (see "EIPs Process" below), you will need to either provide an implementation for clients or convince clients to implement your EIP. +For Core EIPs, given that they require client implementations to be considered **Final** (see "EIPs Process" below), you will need to either provide an implementation for clients or convince clients to implement your EIP. The best way to get client implementers to review your EIP is to present it on an AllCoreDevs call. You can request to do so by posting a comment linking your EIP on an [AllCoreDevs agenda GitHub Issue](https://github.com/ethereum/pm/issues). @@ -64,9 +65,9 @@ These calls generally result in a "rough consensus" around what EIPs should be i :warning: The EIPs process and AllCoreDevs call were not designed to address contentious non-technical issues, but, due to the lack of other ways to address these, often end up entangled in them. This puts the burden on client implementers to try and gauge community sentiment, which hinders the technical coordination function of EIPs and AllCoreDevs calls. If you are shepherding an EIP, you can make the process of building community consensus easier by making sure that [the Ethereum Magicians forum](https://ethereum-magicians.org/) thread for your EIP includes or links to as much of the community discussion as possible and that various stakeholders are well-represented. -*In short, your role as the champion is to write the EIP using the style and format described below, shepherd the discussions in the appropriate forums, and build community consensus around the idea.* +*In short, your role as the champion is to write the EIP using the style and format described below, shepherd the discussions in the appropriate forums, and build community consensus around the idea.* -### EIP Process +### EIP Process The following is the standardization process for all EIPs in all tracks: @@ -84,7 +85,7 @@ If this period results in necessary normative changes it will revert the EIP to **Final** - This EIP represents the final standard. A Final EIP exists in a state of finality and should only be updated to correct errata and add non-normative clarifications. -**Stagnant** - Any EIP in `Draft` or `Review` or `Last Call` if inactive for a period of 6 months or greater is moved to `Stagnant`. An EIP may be resurrected from this state by Authors or EIP Editors through moving it back to `Draft` or it's earlier status. If not resurrected, a proposal may stay forever in this status. +**Stagnant** - Any EIP in `Draft` or `Review` or `Last Call` if inactive for a period of 6 months or greater is moved to `Stagnant`. An EIP may be resurrected from this state by Authors or EIP Editors through moving it back to `Draft` or it's earlier status. If not resurrected, a proposal may stay forever in this status. >*EIP Authors are notified of any algorithmic change to the status of their EIP* @@ -98,12 +99,12 @@ Each EIP should have the following parts: - Preamble - RFC 822 style headers containing metadata about the EIP, including the EIP number, a short descriptive title (limited to a maximum of 44 characters), a description (limited to a maximum of 140 characters), and the author details. Irrespective of the category, the title and description should not include EIP number. See [below](./eip-1.md#eip-header-preamble) for details. - Abstract - Abstract is a multi-sentence (short paragraph) technical summary. This should be a very terse and human-readable version of the specification section. Someone should be able to read only the abstract to get the gist of what this specification does. -- Motivation _(optional)_ - A motivation section is critical for EIPs that want to change the Ethereum protocol. It should clearly explain why the existing protocol specification is inadequate to address the problem that the EIP solves. This section may be omitted if the motivation is evident. +- Motivation *(optional)* - A motivation section is critical for EIPs that want to change the Ethereum protocol. It should clearly explain why the existing protocol specification is inadequate to address the problem that the EIP solves. This section may be omitted if the motivation is evident. - Specification - The technical specification should describe the syntax and semantics of any new feature. The specification should be detailed enough to allow competing, interoperable implementations for any of the current Ethereum platforms (cpp-ethereum, go-ethereum, parity, ethereumJ, ethereumjs-lib, [and others](https://ethereum.org/en/developers/docs/nodes-and-clients). - Rationale - The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g. how the feature is supported in other languages. The rationale should discuss important objections or concerns raised during discussion around the EIP. -- Backwards Compatibility _(optional)_ - All EIPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their consequences. The EIP must explain how the author proposes to deal with these incompatibilities. This section may be omitted if the proposal does not introduce any backwards incompatibilities, but this section must be included if backward incompatibilities exist. -- Test Cases _(optional)_ - Test cases for an implementation are mandatory for EIPs that are affecting consensus changes. Tests should either be inlined in the EIP as data (such as input/expected output pairs, or included in `../assets/eip-###/`. This section may be omitted for non-Core proposals. -- Reference Implementation _(optional)_ - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. This section may be omitted for all EIPs. +- Backwards Compatibility *(optional)* - All EIPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their consequences. The EIP must explain how the author proposes to deal with these incompatibilities. This section may be omitted if the proposal does not introduce any backwards incompatibilities, but this section must be included if backward incompatibilities exist. +- Test Cases *(optional)* - Test cases for an implementation are mandatory for EIPs that are affecting consensus changes. Tests should either be inlined in the EIP as data (such as input/expected output pairs, or included in `../assets/eip-###/`. This section may be omitted for non-Core proposals. +- Reference Implementation *(optional)* - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. This section may be omitted for all EIPs. - Security Considerations - All EIPs must contain a section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks and can be used throughout the life-cycle of the proposal. E.g. include security-relevant design decisions, concerns, important discussions, implementation-specific guidance and pitfalls, an outline of threats and risks and how they are being addressed. EIP submissions missing the "Security Considerations" section will be rejected. An EIP cannot proceed to status "Final" without a Security Considerations discussion deemed sufficient by the reviewers. - Copyright Waiver - All EIPs must be in the public domain. The copyright waiver MUST link to the license file and use the following wording: `Copyright and related rights waived via [CC0](../LICENSE.md).` @@ -143,7 +144,7 @@ Headers that permit lists must separate elements with commas. Headers requiring dates will always do so in the format of ISO 8601 (yyyy-mm-dd). -#### `author` header +### `author` header The `author` header lists the names, email addresses or usernames of the authors/owners of the EIP. Those who prefer anonymity may use a username only, or a first name and a username. The format of the `author` header value must be: @@ -163,25 +164,25 @@ It is not possible to use both an email and a GitHub username at the same time. At least one author must use a GitHub username, in order to get notified on change requests and have the capability to approve or reject them. -#### `discussions-to` header +### `discussions-to` header While an EIP is a draft, a `discussions-to` header will indicate the URL where the EIP is being discussed. The preferred discussion URL is a topic on [Ethereum Magicians](https://ethereum-magicians.org/). The URL cannot point to Github pull requests, any URL which is ephemeral, and any URL which can get locked over time (i.e. Reddit topics). -#### `type` header +### `type` header The `type` header specifies the type of EIP: Standards Track, Meta, or Informational. If the track is Standards please include the subcategory (core, networking, interface, or ERC). -#### `category` header +### `category` header The `category` header specifies the EIP's category. This is required for standards-track EIPs only. -#### `created` header +### `created` header The `created` header records the date that the EIP was assigned a number. Both headers should be in yyyy-mm-dd format, e.g. 2001-08-14. -#### `requires` header +### `requires` header EIPs may have a `requires` header, indicating the EIP numbers that this EIP depends on. @@ -213,7 +214,7 @@ The current EIP editors are - Matt Garnett (@lightclient) - Sam Wilson (@SamWilsn) -Emeritus EIP editors are +Emeritus EIP editors are - Casey Detrio (@cdetrio) - Hudson Jameson (@Souptacular) @@ -251,25 +252,25 @@ The editors don't pass judgment on EIPs. We merely do the administrative & edito The `title` field in the preamble: - - Should not include the word "standard" or any variation thereof; and - - Should not include the EIP's number. +- Should not include the word "standard" or any variation thereof; and +- Should not include the EIP's number. ### Descriptions The `description` field in the preamble: - - Should not include the word "standard" or any variation thereof; and - - Should not include the EIP's number. +- Should not include the word "standard" or any variation thereof; and +- Should not include the EIP's number. ### EIP numbers When referring to an EIP by number, it should be written in the hyphenated form `EIP-X` where `X` is the EIP's assigned number. -### RFC 2119 +### RFC 2119 and RFC 8174 -EIPs are encouraged to follow [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) for terminology and to insert the following at the beginning of the Specification section: +EIPs are encouraged to follow [RFC 2119](https://www.ietf.org/rfc/rfc2119.html) and [RFC 8174](https://www.ietf.org/rfc/rfc8174.html) for terminology and to insert the following at the beginning of the Specification section: -> The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. +> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ## History diff --git a/EIPS/eip-1271.md b/EIPS/eip-1271.md index f9bbdd8c74ecae..55da665aef721e 100644 --- a/EIPS/eip-1271.md +++ b/EIPS/eip-1271.md @@ -127,12 +127,7 @@ Example implementation of a signing contract: } // Recover ECDSA signer - signer = ecrecover( - keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash)), - v, - r, - s - ); + signer = ecrecover(_hash, v, r, s); // Prevent signer from being 0x0 require( diff --git a/EIPS/eip-2135.md b/EIPS/eip-2135.md index 17c386c2fb7f34..6c3d32582c5816 100644 --- a/EIPS/eip-2135.md +++ b/EIPS/eip-2135.md @@ -1,6 +1,6 @@ --- eip: 2135 -title: Consumable NFT for Ticketing +title: Consumable Interface (Tickets, etc) description: An interface extending EIP-721 and EIP-1155 for consumability, supporting use case such as an event ticket. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/eip-2135-erc-consumable-interface/3439 @@ -12,9 +12,11 @@ requires: 165, 721, 1155 --- ## Abstract -This EIP defines an interface to mark a digital asset as "consumable" and to react to its "consumption." + +This EIP defines an interface to mark a digital asset as "consumable" and to react to its "consumption." ## Motivation + Digital assets sometimes need to be consumaed. One of the most common examples is a concert ticket. It is "consumed" when the ticket-holder enters the concert hall. @@ -29,36 +31,51 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity pragma solidity >=0.7.0 <0.9.0; -/// The EIP-165 identifier of this interface is 0x0c44c20d -interface IEIP2135 { - - /// @notice The consume function consumes a token every time it succeeds. - /// @param _consumer the address of consumer of this token. It doesn't have - /// to be the EOA or contract Account that initiates the TX. - /// @param _assetId the NFT asset being consumed - /// @param _data extra data passed in for consume for extra message - /// or future extension. - function consume(address _consumer, uint256 _assetId, bytes[] calldata _data) external returns(bool _success); - - /// @notice The interface to check whether an asset is consumable. - /// @param _consumer the address of consumer of this token. It doesn't have - /// to be the EOA or contract Account that initiates the TX. - /// @param _assetId the NFT asset being consumed - function isConsumableBy(address _consumer, uint256 _assetId) external view returns (bool _consumable); - - /// @notice The event emitted when there is a successful consumption. - /// @param _consumer the address of consumer of this token. It doesn't have - /// to be the EOA or contract Account that initiates the TX. - /// @param _assetId the NFT asset being consumed - /// @param _data extra data passed in for consume for extra message - /// or future extension. - event OnConsumption(address indexed _consumer, uint256 indexed _assetId, bytes[] _data); +/// The EIP-165 identifier of this interface is 0xdd691946 +interface IERC2135 { + /// @notice The consume function consumes a token every time it succeeds. + /// @param _consumer the address of consumer of this token. It doesn't have + /// to be the EOA or contract Account that initiates the TX. + /// @param _assetId the NFT asset being consumed + /// @param _data extra data passed in for consume for extra message + /// or future extension. + function consume( + address _consumer, + uint256 _assetId, + uint256 _amount, + bytes calldata _data + ) external returns (bool _success); + + /// @notice The interface to check whether an asset is consumable. + /// @param _consumer the address of consumer of this token. It doesn't have + /// to be the EOA or contract Account that initiates the TX. + /// @param _assetId the NFT asset being consumed. + /// @param _amount the amount of the asset being consumed. + function isConsumableBy( + address _consumer, + uint256 _assetId, + uint256 _amount + ) external view returns (bool _consumable); + + /// @notice The event emitted when there is a successful consumption. + /// @param consumer the address of consumer of this token. It doesn't have + /// to be the EOA or contract Account that initiates the TX. + /// @param assetId the NFT asset being consumed + /// @param amount the amount of the asset being consumed. + /// @param data extra data passed in for consume for extra message + /// or future extension. + event OnConsumption( + address indexed consumer, + uint256 indexed assetId, + uint256 amount, + bytes data + ); } ``` 2. If the compliant contract is an [EIP-721](./eip-721.md) or [EIP-1155](./eip-1155.md) token, in addition to `OnConsumption`, it **MUST** also emit the `Transfer` / `TransferSingle` event (as applicable) as if a token has been transferred from the current holder to the zero address if the call to `consume` method succeeds. -3. `supportsInterface(0x0c44c20d)` **MUST** return `true` for any compliant contract, as per [EIP-165](./eip-165.md). +3. `supportsInterface(0xdd691946)` **MUST** return `true` for any compliant contract, as per [EIP-165](./eip-165.md). ## Rationale @@ -91,6 +108,49 @@ consumable by the `_consumer`. This interface is designed to be compatible with EIP-721 and NFT of EIP-1155. It can be tweaked to used for [EIP-20](./eip-20.md), [EIP-777](./eip-777.md) and Fungible Token of EIP-1155. +## Test Cases + +```ts + + describe("Consumption", function () { + it("Should consume when minted", async function () { + const fakeTokenId = "0x1234"; + const { contract, addr1 } = await loadFixture(deployFixture); + await contract.safeMint(addr1.address, fakeTokenId); + expect(await contract.balanceOf(addr1.address)).to.equal(1); + expect(await contract.ownerOf(fakeTokenId)).to.equal(addr1.address); + expect(await contract.isConsumableBy(addr1.address, fakeTokenId, 1)).to.be.true; + const tx = await contract.consume(addr1.address, fakeTokenId, 1, []); + const receipt = await tx.wait(); + const events = receipt.events.filter((x: any) => { return x.event == "OnConsumption" }); + expect(events.length).to.equal(1); + expect(events[0].args.consumer).to.equal(addr1.address); + expect(events[0].args.assetId).to.equal(fakeTokenId); + expect(events[0].args.amount).to.equal(1); + expect(await contract.balanceOf(addr1.address)).to.equal(0); + await expect(contract.ownerOf(fakeTokenId)) + .to.be.rejectedWith('ERC721: invalid token ID'); + await expect(contract.isConsumableBy(addr1.address, fakeTokenId, 1)) + .to.be.rejectedWith('ERC721: invalid token ID'); + }); + }); + + describe("EIP-165 Identifier", function () { + it("Should match", async function () { + const { contract } = await loadFixture(deployFixture); + expect(await contract.get165()).to.equal("0xdd691946"); + expect(await contract.supportsInterface("0xdd691946")).to.be.true; + }); + }); +``` + +## Reference Implementation + +A deployment of version 0x1002 has been deployed onto `goerli` testnet at address `0x3682bcD67b8A5c0257Ab163a226fBe07BF46379B`. + +Find the reference contract verified source code on Etherscan's +`goerli` site for the address above. + ## Security Considerations Compliant contracts should pay attention to the balance change when a token is consumed. diff --git a/EIPS/eip-2294.md b/EIPS/eip-2294.md new file mode 100644 index 00000000000000..9837e6ad5dc67c --- /dev/null +++ b/EIPS/eip-2294.md @@ -0,0 +1,59 @@ +--- +eip: 2294 +title: Explicit bound to Chain ID size +description: Adds a maximum value to the Chain ID parameter to avoid potential encoding issues that may occur when using large values of the parameter. +author: Zainan Victor Zhou (@xinbenlv), Alex Beregszaszi (@axic) +discussions-to: https://ethereum-magicians.org/t/eip-2294-explicit-bound-to-chain-id/11090 +status: Review +type: Standards Track +category: Core +created: 2019-09-19 +requires: 155 +--- + +## Abstract + +Starting from `blocknum = FORK_BLKNUM`, this EIP restricts the size of the [EIP-155](./eip-155.md) Chain ID parameter to a particular maximum value `floor(MAX_UINT64 / 2) - 36`, in order to ensure that there is a standard around how this parameter is to be used between different projects. + +## Motivation + +EIP-155 introduces the Chain ID parameter, which is an important parameter used for domain separation (replay protection) of Ethereum protocol signed messages. However, it does not specify any properties about the size that this parameter takes. Allowing it to be 256-bit wide means that the RLP encoding of a transaction must use >256-bit arithmetic to calculate the v field. + +and suggests a reasonable maximum enforced size in order to ensure that there are no issues when encoding this parameter. This would allow a sufficient amount of different values for this parameter, which is typically chosen by community consensus as a genesis parameter for a given chain and thus does not change often. + + +Without a well-chosen value of Chain ID, there could be differences in the implementation of [EIP-155](./eip-155.md) (and [EIP-1344](./eip-1344.md) by derivative) in both client codebase and external tooling that could lead to consensus-critical vulnerabilities being introduced to the network. By making this limit explicit, we avoid this scenario for Ethereum and any project which uses the Ethereum codebase. + +There have been suggestions of using a hash-based identifier in place on Chain ID to allow the value to adapt over time to different contentious forks and other scenarios. This proposal does not describe this behavior, but ~63 bits of entropy should be enough to ensure that no collisions are likely for reasonable (e.g. non-malicious) uses of this feature for that purpose. + +Also, the field `chainID` have experienced increasing usage and dependencies, due more and more contracts are depending on [EIP-1344](./eip-1344.md) to expose CHAIN ID in the smart contract execution. For example when used with [EIP-712](./eip-712.md), [EIP-1271](./eip-1271.md) for on-contract signature verification, chainId has been increasingly introduced for replay attack prevention. It's security critical to ensure clients depending on the chainId computation in cryptography yields identical result for verification in +all cases. + +Originally, this EIP was created by Bryant Eisenbach (@fubuloubu) and Alex Beregszaszi (@axic). + +## Specification + +Starting from `blocknum = FORK_BLKNUM`, the maximum value of Chain ID is `9,223,372,036,854,775,771` (`MAX_CHAIN_ID`). + +1. Compliant client MUST reject a value outside of the range of `[0, MAX_CHAIN_ID]` in a provided transaction starting from `blocknum = FORK_BLKNUM` +2. Compliant client MUST disallow a genesis configuration with a value for Chain ID outside of this limit. + +Due to how the calculation for chain ID is performed, the maximum value seen during the arithmetic is `CHAIN_ID * 2 + 36`, so clients must test to ensure no overflow conditions are encountered when the highest value is used. No underflow is possible. + +## Rationale + +The `MAX_CHAIN_ID` is calculated to avoid overflow when performing uint64 math. For reference, a value of 0 or less is also disallowed. + +## Backwards Compatibility + +This EIP introduces a change that affects previous implementations of this feature. However, as of time of writing(2022-10-18) no known chain makes use of a value outside of the suggested bounds, there should not be an issue in adopting this limit on the size of this parameter, therefore the impact should be non-existent. + +If any other chain is operating with a incompatible `chainId`, we advised they make proper arrangement when this EIP becomes adopted. + +## Security Considerations + +Needs discussion. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-2315.md b/EIPS/eip-2315.md index 3abe276052f57e..a74b9260f8dbf7 100644 --- a/EIPS/eip-2315.md +++ b/EIPS/eip-2315.md @@ -16,10 +16,12 @@ requires: 3540, 3670, 4200 This proposal provides a _complete_, _efficient_, _safe_ and _static_ control-flow facility. It introduces two new opcodes to support calling and returning from subroutines: + * `RJUMPSUB relative_offset` -- relative jump to subroutine * `RETURNSUB` -- return to `PC` after most recent `RJUMPSUB`. It depends on the two new opcodes proposed by [EIP-4200](./eip-4200.md) to support static jumps: + * `RJUMP relative_offset` — relative jump to `PC + relative_offset` * `RJUMPI relative_offset` — conditional relative jump @@ -31,42 +33,43 @@ This is among the simplest possible proposals that meets these requirements. ## Motivation -### We need a complete control-flow facility. +### A complete control-flow facility. Jumps, conditional jumps and subroutines were proposed by Alan Turing in 1945 as a means of organizing the logic of the code and the design of the memory crystals for his Automatic Computing Engine: > We wish to be able to arrange that sequences of orders can divide at various points, continuing in different ways according to the outcome of the calculations to date... We also wish to be able to arrange for the splitting up of operations into subsidiary operations... To start on a subsidiary operation we need only make a note of where we left off the major operation and then apply the first instruction of the subsidiary. When the subsidiary is over we look up the note and continue with the major operation. -> -> — Alan Turing — *in B.E. Carpenter, R.W. Doran, "The other Turing machine." The Computer Journal, Volume 20, Issue 3, January 1977.* +> +> — Alan Turing — in B.E. Carpenter, R.W. Doran, "The other Turing machine." The Computer Journal, Volume 20, Issue 3, January 1977. In more contemporary terms, we have sequences of instructions, jumps and conditional jumps that divide sequences into blocks, subroutine calls, and a stack of addresses to return to. The details vary, but similar facilities have proven their value across a long line of important machines over the last 75 years, including most all of the machines we have programmed or implemented -- physical machines including the Burroughs 5000, CDC 7600, IBM 360, DEC PDP-11 and VAX, Motorola 68000, Sun SPARC, and Intel x86s, as well as virtual machines for Scheme, Forth, Pascal, Java, Wasm, and others. Unlike these machines, the Ethereum Virtual Machine _does not_ provide subroutine operations. Instead, they must be synthesized using the dynamic `JUMP` instruction, which takes its destination on the stack. Further, the EVM provides _only_ dynamic jumps, impeding the static analysis we need. -### We need efficient control-flow. +### Efficient control-flow. Efficient to write by hand, compile from high level labguages, validate at deploy time, interpret by VMs, and compile to machine code. Static jumps, conditional jumps, and subroutines are sufficient and efficient in space and time, as shown by historical experience and as we will show for the EVM below. -### We need safe control-flow. +### Safe control-flow. -The EVM has unusually high requirements for safety. Not only do many smart contracts control inordinately large amounts of valuable Ether, but once placed on the blockchain any defects are visible to attackers and cannot be repaired. We need to statically validate important safety constraints on code at initialization time. +The EVM has unusually high requirements for safety. Not only do many smart contracts control inordinately large amounts of valuable Ether, but once placed on the blockchain any defects are visible to attackers and cannot be repaired. We propose to statically validate important safety constraints on code at initialization time. -### We need static control-flow. +### Static control-flow. The EVM's dynamic jumps cause two major problems. First, the need to synthesize static jumps and subroutines with dynamic jumps wastes space and gas with needlessly complex code, as we will show below. Worse, jumps that can dynamically branch to any destination in the code can cause quadratic "path explosions" when traversing the flow of control. For Ethereum, this is a denial-of-service vulnerability that prevents us, at initialization time, from validating the safe use of EVM code and from compiling EVM code to machine code. -We need static control-flow to support streaming, one-pass, and other near-linear compilers of EVM bytecode to machine code. +We _need_ static control-flow to validate program safety and to compile EVM bytecode to machine code -- in time and space linear in the size of the code. ## Specification ### Opcodes -#### `RJUMPSUB (0x5f) relative_offset +#### `RJUMPSUB (0x5f) relative_offset` Transfers control to a subroutine. + 1. Decode the `relative_offset` from the immediate data at `PC`. 2. Push the current `PC + 3` to the `return stack`. 3. Set `PC` to `PC + relative_offset`. @@ -78,24 +81,28 @@ The gas cost is _low_. #### `RETURNSUB (0x5e)` Returns control to the caller of a subroutine. + 1. Pop the `return stack` to `PC`. The gas cost is _verylow_. _Notes:_ -* _Values popped off the `return stack` do not need to be validated, since they are alterable only by `RJUMPSUB` and `RETURNSUB`._ + +* _Values popped off the `return stack` do not need to be validated, since they are alterable only by `RJUMPSUB` and `RETURNSUB`._ * _The description above lays out the semantics of these instructions in terms of a `return stack`. But the actual state of the `return stack` is not observable by EVM code or consensus-critical to the protocol. (For example, a node implementer may code `RJUMPSUB` to unobservably push `PC` on the `return stack` rather than `PC + 1`, which is allowed so long as `RETURNSUB` observably returns control to the `PC + 3` location.)_ ### Validity -If the execution of an instruction would violate a condition, then the execution is in an exceptional halting state. The Yellow Paper defines five such states. +_Execution_ is defined in the Yellow Paper as a sequence of changes in the EVM state. The conditions on valid code are preserved by state changes. At runtime, if execution of an instruction would violate a condition the execution is in an exceptional halting state. The Yellow Paper defines six such states. + 1. Insufficient gas 2. More than 1024 stack items -3. Insufficient stack items -4. Invalid jump destination -5. Invalid instruction +3. State modification during a static call +4. Insufficient stack items +5. Invalid jump destination +6. Invalid instruction -We would like to consider EVM code valid iff no execution of the program can lead to an exceptional halting state. In practice, we must test at runtime for conditions 1 and 2 — sufficient gas and sufficient stack. We don’t know how much gas there will be, we don’t know how deep a recursion may go, and analysis of stack depth even for non-recursive programs is nontrivial. All of the remaining conditions MUST be validated statically, in time and space quasi-linear in the size of the code. +We would like to consider EVM code valid iff no execution of the program can lead to an exceptional halting state. In practice, we must test at runtime for the first three conditions. We don’t know how much gas there will be, we don’t know how deep a recursion may go, analysis of stack depth even for non-recursive programs is nontrivial, and we don't know whether a call will be static. All of the remaining conditions MUST be validated statically, in time and space quasi-linear in the size of the code. #### Static Constraints on Valid Code @@ -109,7 +116,7 @@ We would like to consider EVM code valid iff no execution of the program can lea * The data stack MUST be consistently aligned: * The data stack height is * the absolute difference between the current `stack pointer` and the `stack pointer` on entry to the current subroutine. - * It MUST be the same for every reachable path through a given `PC` and + * It MUST be the same for every reachable path through a given `PC` and * MUST NOT exceed 1024. ## Rationale @@ -127,11 +134,12 @@ The constraints on valid code cover all of the exceptional halting states that w The `RJUMP`, `RJUMPI` and `RJUMPSUB` instructions take their *relative_offset* as immediate arguments, which cannot change at runtime. Having constant destinations for all jumps means that all jump destinations can be validated at initialization time, not runtime. Dynamic jumps can branch to any destination in the code, so exploitable quadratic "path explosions" are possible when traversing the control flow graph. Deprecating `JUMP` and `JUMPI` prevents this. Requiring a consistently aligned `data stack` + * prevents stack underflow * ensures that all calls to a subroutine have the same number of inputs and the same number of outputs and * ensures that stack height is bounded in the absence of recursion. -Requiring a consistently aligned `data stack` also allows some algorithms that traverse the control-flow graph -- including code validation and compilation -- to break cycles at joins, again preventing quadratic path explosion. When a traversal gets to a `PC` it has visited before it is either at the beginning of a loop or the entry to a function -- and the stacks will align. So we know that loops will not grow stack, and that the number of arguments to a subroutine will always be the same -- there is no need to traverse that path again. +Requiring a consistently aligned `data stack` also allows some algorithms that traverse the control-flow graph -- including code validation and compilation -- to break cycles at joins, again preventing quadratic path explosion. When a traversal gets to a `PC` it has visited before it is either at the beginning of a loop or the entry to a function. Since the stack height at that `PC` is constant we know that loops will not grow stack, and that the number of arguments to a subroutine will always be the same -- there may be no need to traverse that path again. _Note: The JVM and Wasm enforce similar constraints for similar reasons._ @@ -141,7 +149,7 @@ There are a few major designs for a subroutine facility, two of which are consid *1. Keep return addresses on a dedicated return stack.* Turing's design is often used by stack machines, including those for Forth, Java, Wasm, and others. The data stack is used for computation, with a dedicated stack for return addresses. A single instruction suffices to call, and another to return from a routine. -*2. Keep return addresses on the data stack.* This design is often used by register machines, including those from CDC, IBM, DEC, Intel, and ARM. The registers are used primarily for computation, and the stack maintains call frames for return addresses, arguments, and local variables. On the EVM there are no registers for computation, so using the stack for both purposes can cause the sort of inefficiencies we saw above. Pascal p-code does use this design, but as part of a complex calling convention with dedicated registers. +*2. Keep return addresses on the data stack.* This design is often used by register machines, including those from CDC, IBM, DEC, Intel, and ARM. The registers are used primarily for computation, and the stack maintains call frames for return addresses, arguments, and local variables. On the EVM there are no registers for computation, so using the stack for both purposes can cause the sort of inefficiencies we see below. Pascal p-code does use this design, but as part of a complex calling convention with dedicated registers. #### We prefer the dedicated return stack. @@ -155,7 +163,7 @@ There are a few major designs for a subroutine facility, two of which are consid ### Efficiency -We illustrate here how subroutine instructions can be used to reduce the complexity and gas costs of both ordinary and optimized subroutine calls compared to using `JUMP`. +We illustrate here how subroutine instructions can be used to reduce the complexity and gas costs of both ordinary and optimized subroutine calls compared to using `JUMP`. #### **Simple Subroutine Call** @@ -167,7 +175,7 @@ SQUARE: dup1 ; 3 gas mul ; 5 gas returnsub ; 3 gas - + CALL_SQUARE: push 0x02 ; 3 gas rjumpsub SQUARE ; 5 gas @@ -315,20 +323,20 @@ Error: at pc=0, op=RETURNSUB: invalid retsub In this example the RJUMPSUB is on the last byte of code. When the subroutine returns, it should hit the 'virtual stop' _after_ the bytecode, and not exit with error -Bytecode: `0x5c035e5ff` (`RJUMP 3, RETURNSUB, RJUMPSUB -1`) +Bytecode: `0x5c00045e5fffff` (`RJUMP 4, RETURNSUB, RJUMPSUB -1`) | Pc | Op | Cost | Stack | RStack | |-------|-------------|------|-----------|-----------| | 0 | RJUMP | 5 | [] | [] | -| 2 | RETURNSUB | 5 | [] | [] | -| 3 | RJUMPSUB | 5 | [] | [] | -| 5 | STOP | 0 | [] | [] | +| 3 | RETURNSUB | 5 | [] | [] | +| 4 | RJUMPSUB | 5 | [] | [] | +| 7 | STOP | 0 | [] | [] | Consumed gas: `15` -## Reference Implementation +## Reference Implementation -The following is a pseudo-Python implementation of an algorithm for predicating code validity. An equivalent algorithm must be run at initialization time. +The following is a pseudo-Python implementation of an algorithm for predicating code validity. An equivalent algorithm must be run at initialization time. This algorithm performs a symbolic execution of the program that recursively traverses the _code_, emulating its control flow and stack use and checking for violations of the rules above. @@ -400,7 +408,7 @@ def validate_code(code: bytes, pc: int, sp: int, bp: int) -> boolean: return false push(return_stack, pc + 3) pc = jumpdest - continue + continue elif opcode == RETURNSUB: diff --git a/EIPS/eip-2535.md b/EIPS/eip-2535.md index 2e7d292dd2d1ad..3b41dc004631d9 100644 --- a/EIPS/eip-2535.md +++ b/EIPS/eip-2535.md @@ -4,8 +4,7 @@ title: Diamonds, Multi-Facet Proxy description: Create modular smart contract systems that can be extended after deployment. author: Nick Mudge (@mudgen) discussions-to: https://ethereum-magicians.org/t/discussion-for-eip2535-diamonds/10459/ -status: Last Call -last-call-deadline: 2022-10-03 +status: Final type: Standards Track category: ERC created: 2020-02-22 @@ -372,8 +371,7 @@ bytes4 functionSelector = bytes4(keccak256("myFunction(uint256)")); // get facet address of function address facet = ds.selectorToFacet[functionSelector]; bytes memory myFunctionCall = abi.encodeWithSelector(functionSelector, 4); -(bool success, uint result) = address(facet).delegatecall(myFunctionCall); -require(success, "myFunction failed"); +(bool success, bytes memory result) = address(facet).delegatecall(myFunctionCall); ``` 6. Instead of calling an external function defined in another facet you can instead create an internal function version of the external function. Add the internal version of the function to the facet that needs to use it. @@ -387,7 +385,7 @@ It is possible to create and deploy a set of facets that are reused by different The ability to use the same deployed facets for many diamonds reduces deployment costs. -Facets can also be designed for a specific diamond and not be reusable in other diamonds. +It is possible to implement facets in a way that makes them usable/composable/compatible with other facets. It is also possible to implement facets in a way that makes them not usable/composable/compatible with other facets. A function signature is the name of a function and its parameter types. Example function signature: `myfunction(uint256)`. A limitation is that two external functions with the same function signature can’t be added to the same diamond at the same time because a diamond, or any contract, cannot have two external functions with the same function signature. @@ -435,4 +433,3 @@ Security and domain experts can review the history of change of a diamond to det ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). - diff --git a/EIPS/eip-2537.md b/EIPS/eip-2537.md index 53731144681424..9a80865676204c 100644 --- a/EIPS/eip-2537.md +++ b/EIPS/eip-2537.md @@ -95,7 +95,7 @@ Base field element (Fp) is encoded as `64` bytes by performing BigEndian encodin For elements of the quadratic extension field (Fp2) encoding is byte concatenation of individual encoding of the coefficients totaling in `128` bytes for a total encoding. For an Fp2 element in a form `el = c0 + c1 * v` where `v` is formal quadratic non-residue and `c0` and `c1` are Fp elements the corresponding byte encoding will be `encode(c0) || encode(c1)` where `||` means byte concatenation (or one can use `bytes32[4]` or `uint256[4]` in terms of Solidity types). -*Note on the top `16` bytes being zero*: it's required that encoded element is "in a field" that means strictly `< modulus`. In BigEndian encoding it automatically means that for a modulus that is just `381` bit long top `16` bytes in `64` bytes encoding are zeroes and it **must** be checked if only a subslice of input data is used for actual decoding. +*Note on the top `16` bytes being zero*: it's required that the encoded element is "in a field" that means strictly `< modulus`. In BigEndian encoding it automatically means that for a modulus that is just `381` bit long top `16` bytes in `64` bytes encoding are zeroes and it **must** be checked if only a subslice of input data is used for actual decoding. If encodings do not follow this spec anywhere during parsing in the precompile the precompile *must* return an error. @@ -105,7 +105,7 @@ Points in either G1 (in base field) or in G2 (in extension field) are encoded as #### Point of infinity encoding: -Also referred as "zero point". For BLS12 curves point with coordinates `(0, 0)` (formal zeroes in Fp or Fp2) is *not* on the curve, so encoding of such point `(0, 0)` is used as a convention to encode point of infinity. +Also referred to as "zero point". For BLS12 curves point with coordinates `(0, 0)` (formal zeroes in Fp or Fp2) is *not* on the curve, so encoding of such point `(0, 0)` is used as a convention to encode point of infinity. #### Encoding of scalars for multiplication operation: @@ -250,11 +250,11 @@ Discounts table as a vector of pairs `[k, discount]`: Cost of the pairing operation is `43000*k + 65000` where `k` is a number of pairs. -#### Fp-to-G1 mappign operation +#### Fp-to-G1 mapping operation Fp -> G1 mapping is `5500` gas. -#### Fp2-to-G2 mappign operation +#### Fp2-to-G2 mapping operation Fp2 -> G2 mapping is `75000` gas diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index dd8142938fa8b2..117c47304a25e2 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -1,81 +1,44 @@ --- eip: 2771 title: Secure Protocol for Native Meta Transactions -author: Ronan Sandford (@wighawag), Liraz Siri (@lirazsiri), Dror Tirosh (@drortirosh), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Hadrien Croubois (@Amxx), Sachin Tomar (@tomarsachin2271), Patrick McCorry (@stonecoldpat), Nicolas Venturo (@nventuro), Fabian Vogelsteller (@frozeman) +description: A contract interface for receiving meta transactions through a trusted forwarder +author: Ronan Sandford (@wighawag), Liraz Siri (@lirazsiri), Dror Tirosh (@drortirosh), Yoav Weiss (@yoavw), Alex Forshtat (@forshtat), Hadrien Croubois (@Amxx), Sachin Tomar (@tomarsachin2271), Patrick McCorry (@stonecoldpat), Nicolas Venturo (@nventuro), Fabian Vogelsteller (@frozeman), Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/erc-2771-secure-protocol-for-native-meta-transactions/4488 -status: Stagnant +status: Review type: Standards Track category: ERC created: 2020-07-01 --- -## Simple Summary - -A contract interface for receiving meta transactions through a trusted -forwarder. - ## Abstract -This ERC defines a minimal contract-level protocol that a compliant Recipient -contract needs to support in order to be capable of accepting a meta -transaction through a compliant Forwarder contract that it trusts to help it -identify the address of the Transaction Signer. - -No EVM-level protocol changes are proposed or required. +This EIP defines a contract-level protocol for `Recipient` contracts to accept meta-transactions through trusted `Forwarder` contracts. No protocol changes are made. `Recipient` contracts are sent the effective `msg.sender` (referred to as `_msgSender()`) and `msg.data` (referred to as `_msgData()`) by appending additional calldata. ## Motivation -There is a growing interest in making it possible for Ethereum contracts to -accept calls from externally owned accounts that do not have ETH to pay for -gas. - -This can be accomplished with meta transactions, which are transactions that -have been: +There is a growing interest in making it possible for Ethereum contracts to accept calls from externally owned accounts that do not have ETH to pay for gas. Solutions that allow for third parties to pay for gas costs are called meta transactions. For the purposes of this EIP, meta transactions are transactions that have been authorized by a **Transaction Signer** and relayed by an untrusted third party that pays for the gas (the **Gas Relay**). -1. Authorized by the **Transaction Signer**. For example, signed by an - externally owned account. -2. Relayed by an untrusted third party that pays for the gas (the **Gas - Relay**) +## Specification -`msg.sender` is a transaction parameter that can be inspected by a contract to -determine who signed the transaction. The integrity of this parameter is -guaranteed by the Ethereum EVM, but for a meta transaction securing -`msg.sender` is insufficient. +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. -The problem is that for a contract that is not natively aware of meta -transactions, the `msg.sender` of the transaction will make it appear to be -coming from the **Gas Relay** and not the **Transaction Signer**. A secure -protocol for a contract to accept meta transactions needs to prevent the **Gas -Relay** from forging, modifying or duplicating requests by the **Transaction -Signer**. +### Definitions -## Specification +**Transaction Signer**: Signs & sends transactions to a Gas Relay -The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", -"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be -interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). +**Gas Relay**: Receives signed requests off-chain from Transaction Signers and pays gas to turn it into a valid transaction that goes through a Trusted Forwarder -Here is an example flow: +**Trusted Forwarder**: A contract trusted by the `Recipient` to correctly verify signatures and nonces before forwarding the request from Transaction Signers -![Example flow](../assets/eip-2771/example-flow.png) +**Recipient**: A contract that accepts meta-transactions through a Trusted Forwarder +### Example Flow -* **Transaction Signer** - entity that signs & sends to request to **Gas - Relay** -* **Gas Relay** - receives a signed request off-chain from **Transaction - Signer** and pays gas to turn it into a valid transaction that goes through -**Trusted Forwarder** -* **Trusted Forwarder** - a contract that is trusted by the `Recipient` to - correctly verify the signature and nonce before forwarding the request from -**Transaction Signer** -* **Recipient** - a contract that can securely accept meta-transactions - through a **Trusted Forwarder** by being compliant with this standard. +![Example flow](../assets/eip-2771/example-flow.png) ### Extracting The Transaction Signer address -The **Trusted Forwarder** is responsible for calling the **Recipient** contract -and MUST append the address of the **Transaction Signer** (20 bytes of data) to -the end of the call data. +The **Trusted Forwarder** is responsible for calling the **Recipient** contract and MUST append the address of the **Transaction Signer** (20 bytes of data) to the end of the call data. For example : @@ -83,15 +46,11 @@ For example : (bool success, bytes memory returnData) = to.call.value(value)(abi.encodePacked(data, from)); ``` -The **Recipient** contract can then extract the **Transaction Signer** address -by performing 3 operations: +The **Recipient** contract can then extract the **Transaction Signer** address by performing 3 operations: -1. Check that the **Forwarder** is trusted. How this is implemented is out of - the scope of this proposal. -2. Extract the **Transaction Signer** address from the last 20 bytes of the - call data and use that as the original `sender` of the transaction (instead of `msg.sender`) -3. If the `msg.sender` is not a trusted forwarder (or if the msg.data is - shorter than 20 bytes), then return the original `msg.sender` as it is. +1. Check that the **Forwarder** is trusted. How this is implemented is out of the scope of this proposal. +2. Extract the **Transaction Signer** address from the last 20 bytes of the call data and use that as the original `sender` of the transaction (instead of `msg.sender`) +3. If the `msg.sender` is not a trusted forwarder (or if the `msg.data` is shorter than 20 bytes), then return the original `msg.sender` as it is. The **Recipient** MUST check that it trusts the Forwarder to prevent it from extracting address data appended from an untrusted contract. This could result @@ -99,49 +58,53 @@ in a forged address. ### Protocol Support Discovery Mechanism -Unless a **Recipient** contract is being used by a particular frontend that -knows that this contract has support for native meta transactions, it would not -be possible to offer the user the choice of using meta-transaction to interact -with the contract. We thus need a mechanism by which the **Recipient** can let -the world know that it supports meta transactions. +Unless a **Recipient** contract is being used by a particular frontend that knows that this contract has support for native meta transactions, it would not be possible to offer the user the choice of using meta-transaction to interact with the contract. We thus need a mechanism by which the **Recipient** can let the world know that it supports meta transactions. -This is especially important for meta transactions to be supported at the Web3 -wallet level. Such wallets may not necessarily know anything about the -**Recipient** contract users may wish to interact with. +This is especially important for meta transactions to be supported at the Web3 wallet level. Such wallets may not necessarily know anything about the **Recipient** contract users may wish to interact with. -As a **Recipient** could trust forwarders with different interfaces and -capabilities (e.g., transaction batching, different message signing formats), -we need to allow wallets to discover which Forwarder is trusted. +As a **Recipient** could trust forwarders with different interfaces and capabilities (e.g., transaction batching, different message signing formats), we need to allow wallets to discover which Forwarder is trusted. -To provide this discovery mechanism a **Recipient** contract MUST implement -this function: +To provide this discovery mechanism a **Recipient** contract MUST implement this function: ```solidity -function isTrustedForwarder(address forwarder) external returns(bool); +function isTrustedForwarder(address forwarder) external view returns(bool); ``` -* That function MUST return true if the forwarder is trusted by the - Recipient. -* That function MUST return false if the forwarder is not trusted. -* That function MUST NOT throw a revert. +`isTrustedForwarder` MUST return `true` if the forwarder is trusted by the Recipient, otherwise it MUST return `false`. `isTrustedForwarder` MUST NOT revert. + +Internally, the **Recipient** MUST then accept a request from forwarder. -Internally, the **Recipient** MUST then accept a request from forwarder +`isTrustedForwarder` function MAY be called on-chain, and as such gas restrictions MUST be put in place. A Gas limit of 50k SHOULD be sufficient to making the decision either inside the contract, or delegating it to another contract and doing some memory access calculations, like querying a mapping. -That function can be called on-chain and as such gas restriction needs to be -put in place. +## Rationale -A Gas limit of 50k is enough for making the decision either inside the -contract, or delegating it to another contract and doing some memory access -calculations, like querying a mapping. +* Make it easy for contract developers to add support for meta + transactions by standardizing the simplest viable contract interface. +* Without support for meta transactions in the recipient contract, an externally owned + account can not use meta transactions to interact with the recipient contract. +* Without a standard contract interface, there is no standard way for a client + to discover whether a recipient supports meta transactions. +* Without a standard contract interface, there is no standard way to send a + meta transaction to a recipient. +* Without the ability to leverage a trusted forwarder every recipient contract + has to internally implement the logic required to accept meta transactions securely. +* Without a discovery protocol, there is no mechanism for a client to discover + whether a recipient supports a specific forwarder. +* Making the contract interface agnostic to the internal implementation + details of the trusted forwarder, makes it possible for a recipient contract + to support multiple forwarders with no change to code. +* `msg.sender` is a transaction parameter that can be inspected by a contract to determine who signed the transaction. The integrity of this parameter is guaranteed by the Ethereum EVM, but for a meta transaction securing `msg.sender` is insufficient. + * The problem is that for a contract that is not natively aware of meta transactions, the `msg.sender` of the transaction will make it appear to be coming from the **Gas Relay** and not the **Transaction Signer**. A secure protocol for a contract to accept meta transactions needs to prevent the **Gas Relay** from forging, modifying or duplicating requests by the **Transaction Signer**. +## Reference Implementation -### Recipient example +### Recipient Example ```solidity contract RecipientExample { function purchaseItem(uint256 itemId) external { address sender = _msgSender(); - ... perform the purchase for sender + // ... perform the purchase for sender } address immutable _trustedForwarder; @@ -165,45 +128,11 @@ contract RecipientExample { } ``` -## Rationale - -* Make it easy for contract developers to add support for meta - transactions by standardizing the simplest viable contract interface. - -* Without support for meta transactions in the recipient contract, an externally owned - account can not use meta transactions to interact with the recipient contract. - -* Without a standard contract interface, there is no standard way for a client - to discover whether a recipient supports meta transactions. - -* Without a standard contract interface, there is no standard way to send a - meta transaction to a recipient. - -* Without the ability to leverage a trusted forwarder every recipient contract - has to internally implement the logic required to accept meta transactions securely. - -* Without a discovery protocol, there is no mechanism for a client to discover - whether a recipient supports a specific forwarder. - -* Making the contract interface agnostic to the internal implementation - details of the trusted forwarder, makes it possible for a recipient contract - to support multiple forwarders with no change to code. - ## Security Considerations -A bad forwarder may allow forgery of the `msg.sender` returned from -`_msgSender()` and allow transactions to appear to be coming from any address. - -This means a recipient contract should be very careful which forwarder it -trusts and whether this can be modified. The power to change the forwarder -trusted by a recipient is equivalent to giving full control over the contract. -If this kind of control over the recipient is acceptable, it is recommended -that only the owner of the recipient contract be able to modify which forwarder -is trusted. Otherwise best to leave it unmodifiable, as in the example above. - -## Implementations +A malicious forwarder may forge the value of `_msgSender()` and effectively send transactions from any address. Therefore, `Recipient` contracts must be very careful in trusting forwarders. If a forwarder is upgradeable, then one must also trust that the contract won't perform a malicious upgrade. -An implementation of a base class for a recipient: [BaseRelayRecipient.sol](https://github.com/opengsn/forwarder/blob/master/contracts/BaseRelayRecipient.sol) +In addition, modifying which forwarders are trusted must be restricted, since an attacker could "trust" their own address to forward transactions, and therefore be able to forge transactions. It is recommended to have the list of trusted forwarders be immutable, and if this is not feasible, then only trusted contract owners should be able to modify it. ## Copyright diff --git a/EIPS/eip-3475.md b/EIPS/eip-3475.md index f25ced1d1932c0..c8b909f94c8712 100644 --- a/EIPS/eip-3475.md +++ b/EIPS/eip-3475.md @@ -51,7 +51,7 @@ pragma solidity ^0.8.0; * @param _transactions is the `Transaction[] calldata` (of type ['classId', 'nonceId', '_amountBonds']) structure defined in the rationale section below. * @dev transferFrom MUST have the `isApprovedFor(_from, _to, _transactions[i].classId)` approval to transfer `_from` address to `_to` address for given classId (i.e for Transaction tuple corresponding to all nonces). */ -// function transferFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [[1, 42, 500000000]]); +// function transferFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]); // transfer from `_from` address, to `_to` address, `500000000` bonds of type class`1` and nonce `42`. function transferFrom(address _from, address _to, Transaction[] calldata _transactions) external; @@ -61,9 +61,9 @@ function transferFrom(address _from, address _to, Transaction[] calldata _transa * @param _from is the address of the holder whose balance is about to decrease. * @param _to is the address of the recipient whose balance is about to increase. * @param _transactions is the `Transaction[] calldata` structure defined in the section `rationale` below. -* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) +* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) */ -// function transferAllowanceFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [[1, 42, 500000000]]); +// function transferAllowanceFrom(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B, [IERC3475.Transaction(1,14,500)]); // transfer from `_from` address, to `_to` address, `500000000` bonds of type class`1` and nonce `42`. function transferAllowanceFrom(address _from,address _to, Transaction[] calldata _transactions) public virtual override @@ -73,9 +73,9 @@ function transferAllowanceFrom(address _from,address _to, Transaction[] calldata * @dev it MUST be issued by a single entity (for instance, a role-based ownable contract that has integration with the liquidity pool of the deposited collateral by `_to` address). * @param `_to` argument is the address to which the bond will be issued. * @param `_transactions` is the `Transaction[] calldata` (ie array of issued bond class, bond nonce and amount of bonds to be issued). -* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) +* @dev transferAllowanceFrom MUST have the `allowance(_from, msg.sender, _transactions[i].classId, _transactions[i].nonceId)` (where `i` looping for [ 0 ...Transaction.length - 1] ) */ -// example: issue(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[[0,5,1000]]); +// example: issue(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); // issues `1000` bonds with a class of `0` to address `0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef` with a nonce of `5`. function issue(address _to, Transaction[] calldata _transaction) external; @@ -88,7 +88,7 @@ function issue(address _to, Transaction[] calldata _transaction) external; * @dev redeem function for a given class, and nonce category MUST BE done after certain conditions for maturity (can be end time, total active liquidity, etc.) are met. * @dev furthermore, it SHOULD ONLY be called by the bank or secondary market maker contract. */ -// redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, [[1,42,500000000]]); +// redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, [IERC3475.Transaction(1,14,500)]); // means “redeem from wallet address(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef), 500000000 of bond class1 and nonce 42. function redeem(address _from, Transaction[] calldata _transactions) external; @@ -101,7 +101,7 @@ function redeem(address _from, Transaction[] calldata _transactions) external; * @dev burn function for a given class, and nonce category MUST BE done only after certain conditions for maturity (can be end time, total active liquidity, etc). * @dev furthermore, it SHOULD ONLY be called by the bank or secondary market maker contract. */ -// burnBond(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[[1,42,500000000]]); +// burnBond(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]); // means burning 500000000 bonds of class 1 nonce 42 owned by address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B. function burn(address _from, Transaction[] calldata _transactions) external; @@ -113,7 +113,7 @@ function burn(address _from, Transaction[] calldata _transactions) external; * @param `_spender` argument is the address of the user who is approved to transfer the bonds. * @param `_transactions` is the `Transaction[] calldata` structure (ie array of tuple with the pairs of (class,nonce, and amount) of the bonds that are to be approved to be spend by _spender). Further defined in the rationale section. */ -// approve(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[[0,1,30000]]); +// approve(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]); // means owner of address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is approved to manage 30000 bonds from class 0 and Nonce 1. function approve(address _spender, Transaction[] calldata _transactions) external; @@ -264,7 +264,7 @@ function isApprovedFor(address _owner, address _operator) external view returns event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions); // eg: -emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[['0','14','500']]); +emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); // issue by address(operator) 500 DBIT-USD Bond(nonce14,class 0) to address 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. /** @@ -273,7 +273,7 @@ emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[['0','14','500 */ event Redeem(address indexed _operator, address indexed _from, uint256 classId, uint256 nonceId, uint256 _amount); //e.g: -emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[[1,14,500]]); +emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]); //emit event when 5000 bonds of class 1, nonce 14 owned by address 0x492Af743654549b12b1B807a9E0e8F397E44236E are being redeemed by 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. /** @@ -281,8 +281,7 @@ emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807 * @dev `Burn` MUST trigger when the bonds are being redeemed via staking (or being invalidated) by the bank contract. * @dev `Burn` MUST trigger when Bonds are burned. This SHOULD not include zero value burning. */ - - emit Burn(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[[1,14,500]]); + emit Burn(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]); //emits event when 5000 bonds of owner 0x492Af743654549b12b1B807a9E0e8F397E44236E of type (class 1, nonce 14) are burned by operator 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. /** @@ -291,7 +290,7 @@ emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807 * @dev Transfer MUST trigger when Bonds are transferred. This SHOULD not include zero value transfers. * @dev Transfer event with the _from `0x0` MUST not create this event(use `event Issued` instead). */ - emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, _to, [[1,4,500]]); +emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, _to, [IERC3475.Transaction(1,14,500)]); // transfer by address(_operator) amount 500 DBIT-USD bonds with (Class 1 and Nonce 14) from 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, to address(_to). event Transfer(address indexed _operator, address indexed _from, address indexed _to, Transaction[] _transactions); @@ -342,7 +341,7 @@ This is done via overriding the function interface for functions `classValues` a } ``` -e.g. in the above example, to get the `symbol` of the given class id, we can use the class id as a key to get the `symbol` value in the values, which then can be used for fetching the detail for instance. +e.g. In the above example, to get the `symbol` of the given class id, we can use the class id as a key to get the `symbol` value in the values, which then can be used for fetching the detail for instance. ## Rationale @@ -388,24 +387,21 @@ The `_amount` is the amount of the bond for which the spender is approved. **AMM optimization**: One of the most obvious use cases of this EIP is the multilayered pool. The early version of AMM uses a separate smart contract and an [EIP-20](./eip-20.md) LP token to manage a pair. By doing so, the overall liquidity inside of one pool is significantly reduced and thus generates unnecessary gas spent and slippage. Using this EIP standard, one can build a big liquidity pool with all the pairs inside (thanks to the presence of the data structures consisting of the liquidity corresponding to the given class and nonce of bonds). Thus by knowing the class and nonce of the bonds, the liquidity can be represented as the percentage of a given token pair for the owner of the bond in the given pool. Effectively, the [EIP-20](./eip-20.md) LP token (defined by a unique smart contract in the pool factory contract) is aggregated into a single bond and consolidated into a single pool. - -- The reason behind the standard's name (abstract storage bond) is its ability to store all the specifications (metadata/values and transaction as defined in the following sections) without needing external storage on-chain/off-chain. - - +- The reason behind the standard's name (abstract storage bond) is its ability to store all the specifications (metadata/values and transaction as defined in the following sections) without needing external storage on-chain/off-chain. ## Backwards Compatibility -Any contract that inherits the interface of this EIP is compatible. This compatibility exists for issuer and receiver of the bonds. Also any client EOA wallet can be compatible with the standard if they are able to sign `issue()` and `redeem()` commands. +Any contract that inherits the interface of this EIP is compatible. This compatibility exists for issuer and receiver of the bonds. Also any client EOA wallet can be compatible with the standard if they are able to sign `issue()` and `redeem()` commands. However, any existing [EIP-20](./eip-20.md) token contract can issue its bonds by delegating the minting role to a bank contract with the interface of this standard built-in. Check out our reference implementation for the correct interface definition. -To ensure the indexing of transactions throughout the bond lifecycle (i.e "Issue", "Redeem" and "Transfer" functions), events cited in specification section MUST be emitted when such transaction is passed. +To ensure the indexing of transactions throughout the bond lifecycle (i.e "Issue", "Redeem" and "Transfer" functions), events cited in specification section MUST be emitted when such transaction is passed. **Note that the this standard interface is also compatible with [EIP-20](./eip-20.md) and [EIP-721](./eip-721.md) and [EIP-1155](./eip-1155.md)interface.** However, creating a separate bank contract is recommended for reading the bonds and future upgrade needs. -Acceptable collateral can be in the form of [EIP-20](./eip-20.md) tokens, [EIP-721](./eip-721.md) tokens, or other bonds represented by the standard.Thus bonds can now represent a collection of collaterals (of the same type) of all fungible/non-fungible or semi-fungible tokens. +Acceptable collateral can be in the form of [EIP-20](./eip-20.md) tokens, [EIP-721](./eip-721.md) tokens, or other bonds represented by the standard. Thus bonds can now represent a collection of collaterals (of the same type) of all fungible/non-fungible or semi-fungible tokens. ## Test Cases @@ -415,9 +411,8 @@ Test-case for the minimal reference implementation is [here](../assets/eip-3475/ - [Interface](../assets/eip-3475/interfaces/IERC3475.sol). - - [Basic Example](../assets/eip-3475/ERC3475.sol). - - This demonstration shows only minimalist implementation. + - This demonstration shows only minimalist implementation. ## Security Considerations @@ -425,7 +420,7 @@ Test-case for the minimal reference implementation is [here](../assets/eip-3475/ - If the owner wants to give a one-time allocation to an address for specific bonds(classId,bondsId), he should call the `function approve()` giving the `Transaction[]` allocated rather than approving all the classes using `setApprovalFor`. -- The `function issue()` can only be called by the bank contract (what we call issuer in this EIP). this can be either a smart contract managing the logic of liquidity management and routing the collateral, but also EOA multi-sig. +- The `function issue()` can only be called by the bank contract (what we call issuer in this EIP). This can be either a smart contract managing the logic of liquidity management and routing the collateral, but also EOA multi-sig. ## Copyright diff --git a/EIPS/eip-3525.md b/EIPS/eip-3525.md index 47f62f12c4cd0c..5f0573edaf9c87 100644 --- a/EIPS/eip-3525.md +++ b/EIPS/eip-3525.md @@ -536,25 +536,25 @@ The purpose of maintaining id-to-address transfer function is to maximize the co ### Design decision: Notification/acceptance mechanism instead of 'Safe Transfer' -EIP-721 and some later token standards introduced 'Safe Transfer' model, for better control of the 'safety' when transferring tokens, this mechanism leaves the choice of different transfer mode (safe/unsafe) to the sender, and may cause some potential problem: +EIP-721 and some later token standards introduced 'Safe Transfer' model, for better control of the 'safety' when transferring tokens, this mechanism leaves the choice of different transfer modes (safe/unsafe) to the sender, and may cause some potential problems: -1. In most situation the sender don't know how to choose between two kinds of transfer methods (safe/unsafe); -2. If the sender calls the `safeTransferFrom` method, the transfer may fail when the recipient contract didn't implements the callback function, even if that contract can receive and manipulate the token with no problem. +1. In most situations the sender does not know how to choose between two kinds of transfer methods (safe/unsafe); +2. If the sender calls the `safeTransferFrom` method, the transfer may fail if the recipient contract did not implement the callback function, even if that contract is capable of receiving and manipulating the token without issue. This EIP defines a simple 'Check, Notify and Response' model for better flexibility as well as simplicity: 1. No extra `safeTransferFrom` methods are needed, all callers only need to call one kind of transfer; -2. All EIP-3525 contracts need to MUST check for the existence of `onERC3525Received` on the recipient contract and call the function when it exists; -3. Any smart contract can implement `onERC3525Received` function for purpose of being notified after receiving values, in this function it can return certain pre-defined value to accept the transfer, or any other value to reject. +2. All EIP-3525 contracts MUST check for the existence of `onERC3525Received` on the recipient contract and call the function when it exists; +3. Any smart contract can implement `onERC3525Received` function for the purpose of being notified after receiving values; this function MUST return 0x009ce20b (i.e. `bytes4(keccak256('onERC3525Received(address,uint256,uint256,uint256,bytes)'))`) if the transfer is accepted, or any other value if the transfer is rejected. -There is a special case for this notification/acceptance mechanism, since EIP-3525 allow value transfer from an address to itself, so that when a smart contract which implements `onERC3525Received` transfers value to itself, this function will also be called. So that the contract is responsible to choose different rules of acceptance between self-value-transfer and receiving value from other addresses. +There is a special case for this notification/acceptance mechanism: since EIP-3525 allows value transfer from an address to itself, when a smart contract which implements `onERC3525Received` transfers value to itself, `onERC3525Received` will also be called. This allows for the contract to implement different rules of acceptance between self-value-transfer and receiving value from other addresses. ### Design decision: Relationship between different approval models For semantic compatibility with EIP-721 as well as the flexibility of value manipulation of tokens, we decided to define the relationships between some of the levels of approval like that: -1. Approval of an id will lead to the ability to partially transfer values from this id by the approved operator, this will simplify the value approval for an id. However, the approval of total values in a token should not lead to the ability to transfer the token entity by the approved operator. -2. `setApprovalForAll` will lead to the ability to partially transfer values from any token, as well as the ability to approve partial transfer of values from any token to a third party, this will simplify the value transfer and approval of all tokens owned by an address. +1. Approval of an id will lead to the ability to partially transfer values from this id by the approved operator; this will simplify the value approval for an id. However, the approval of total values in a token should not lead to the ability to transfer the token entity by the approved operator. +2. `setApprovalForAll` will lead to the ability to partially transfer values from any token, as well as the ability to approve partial transfer of values from any token to a third party; this will simplify the value transfer and approval of all tokens owned by an address. ## Backwards Compatibility @@ -566,9 +566,9 @@ As mentioned in the beginning, this EIP is backward compatible with EIP-721. ## Security Considerations -The value level approval and slot level approval(optional) is isolated from EIP-721 approval models, so that approving value should not affect EIP-721 level approvals, implementations of this EIP must obey this principle. +The value level approval and slot level approval (optional) is isolated from EIP-721 approval models, so that approving value should not affect EIP-721 level approvals. Implementations of this EIP must obey this principle. -Since this EIP is EIP-721 compatible, any wallets and smart contracts that can hold and manipulate standard EIP-721 tokens will have no risks of asset loss for EIP-3525 tokens. +Since this EIP is EIP-721 compatible, any wallets and smart contracts that can hold and manipulate standard EIP-721 tokens will have no risks of asset loss for EIP-3525 tokens due to incompatible standards implementations. ## Copyright diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index a7630055e471dd..bbfb8c01336497 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -1,12 +1,12 @@ --- eip: 4337 -title: Account Abstraction via Entry Point Contract specification +title: Account Abstraction using alt mempool description: An account abstraction proposal which completely avoids consensus-layer protocol changes, instead relying on higher-layer infrastructure. author: Vitalik Buterin (@vbuterin), Yoav Weiss (@yoavw), Kristof Gazso (@kristofgazso), Namra Patel (@namrapatel), Dror Tirosh (@drortirosh), Shahaf Nacson (@shahafn), Tjaden Hess (@tjade273) discussions-to: https://ethereum-magicians.org/t/erc-4337-account-abstraction-via-entry-point-contract-specification/7160 +status: Draft type: Standards Track category: ERC -status: Draft created: 2021-09-29 --- @@ -16,11 +16,11 @@ An account abstraction proposal which completely avoids the need for consensus-l ## Motivation -See also ["Implementing Account Abstraction as Part of Eth 1.x"](https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020) and the links therein for historical work and motivation, and [EIP-2938](./eip-2938.md) for a consensus layer proposal for implementing the same goal. +See also `https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020` and the links therein for historical work and motivation, and [EIP-2938](./eip-2938.md) for a consensus layer proposal for implementing the same goal. This proposal takes a different approach, avoiding any adjustments to the consensus layer. It seeks to achieve the following goals: -* **Achieve the key goal of account abstraction**: allow users to use smart contract wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and EIP-3074 both require) +* **Achieve the key goal of account abstraction**: allow users to use smart contract wallets containing arbitrary verification logic instead of EOAs as their primary account. Completely remove any need at all for users to also have EOAs (as status quo SC wallets and [EIP-3074](./eip-3074.md) both require) * **Decentralization** * Allow any bundler (think: miner) to participate in the process of including account-abstracted user operations * Work with all activity happening over a public mempool; users do not need to know the direct communication addresses (eg. IP, onion) of any specific actors @@ -29,25 +29,35 @@ This proposal takes a different approach, avoiding any adjustments to the consen * **Try to support other use cases** * Privacy-preserving applications * Atomic multi-operations (similar goal to EIP-3074) - * Pay tx fees with ERC-20 tokens, allow developers to pay fees for their users, and [EIP-3074](./eip-3074.md)-like **sponsored transaction** use cases more generally + * Pay tx fees with [EIP-20](./eip-20.md) tokens, allow developers to pay fees for their users, and [EIP-3074](./eip-3074.md)-like **sponsored transaction** use cases more generally + * Support aggregated signature (e.g. BLS) ## Specification +### Definitions + +* **UserOperation** - a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is named "transaction". + * Like a transaction, it contains "sender", "to", "calldata", "maxFeePerGas", "maxPriorityFee", "signature", "nonce" + * unlike transaction, it contains several other fields, desribed below + * also, the "nonce" and "signature" fields usage is not defined by the protocol, but by each account implementation +* **Sender** - the account contract sending a user operation. +* **EntryPoint** - a singleton contract to execute bundles of UserOperations. Bundlers/Clients whitelist the supported entrypoint. +* **Aggregator** - a helper contract trusted by wallets to validate an aggregated signature. Bundlers/Clients whitelist the supported aggregators. + To avoid Ethereum consensus changes, we do not attempt to create new transaction types for account-abstracted transactions. Instead, users package up the action they want their wallet to take in an ABI-encoded struct called a `UserOperation`: | Field | Type | Description | - | - | - | | `sender` | `address` | The wallet making the operation | | `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time wallet creation | -| `initCode` | `bytes` | The initCode of the wallet (only needed if the wallet is not yet on-chain and needs to be created) | +| `initCode` | `bytes` | The initCode of the wallet (needed if and only if the wallet is not yet on-chain and needs to be created) | | `callData` | `bytes` | The data to pass to the `sender` during the main execution call | -| `callGas` | `uint256` | The amount of gas to allocate the main execution call | -| `verificationGas` | `uint256` | The amount of gas to allocate for the verification step | +| `callGasLimit` | `uint256` | The amount of gas to allocate the main execution call | +| `verificationGasLimit` | `uint256` | The amount of gas to allocate for the verification step | | `preVerificationGas` | `uint256` | The amount of gas to pay for to compensate the bundler for pre-verification execution and calldata | -| `maxFeePerGas` | `uint256` | Maximum fee per gas (similar to EIP 1559 `max_fee_per_gas`) | -| `maxPriorityFeePerGas` | `uint256` | Maximum priority fee per gas (similar to EIP 1559 `max_priority_fee_per_gas`) | -| `paymaster` | `address` | Address sponsoring the transaction (or zero for regular self-sponsored transactions) | -| `paymasterData` | `bytes` | Extra data to send to the paymaster | +| `maxFeePerGas` | `uint256` | Maximum fee per gas (similar to [EIP-1559](./eip-1559.md) `max_fee_per_gas`) | +| `maxPriorityFeePerGas` | `uint256` | Maximum priority fee per gas (similar to EIP-1559 `max_priority_fee_per_gas`) | +| `paymasterAndData` | `bytes` | Address of paymaster sponsoring the transaction, followed by extra data to send to the paymaster (empty for self-sponsored transaction) | | `signature` | `bytes` | Data passed into the wallet along with the nonce during the verification step | Users send `UserOperation` objects to a dedicated user operation mempool. A specialized class of actors called **bundlers** (either miners running special-purpose code, or users that can relay transactions to miners eg. through a bundle marketplace such as Flashbots that can guarantee next-block-or-never inclusion) listen in on the user operation mempool, and create **bundle transactions**. A bundle transaction packages up multiple `UserOperation` objects into a single `handleOps` call to a pre-published global **entry point contract**. @@ -57,46 +67,124 @@ To prevent replay attacks (both cross-chain and multiple `EntryPoint` implementa The core interface of the entry point contract is as follows: ```c++ -function handleOps - (UserOperation[] calldata ops, address payable beneficiary) - public; +function handleOps(UserOperation[] calldata ops, address payable beneficiary); + +function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary +); function simulateValidation - (UserOperation calldata userOp) - external returns (uint256 preOpGas, uint256 prefund) { + (UserOperation calldata userOp, bool offChainSigCheck) + external returns (uint256 preOpGas, uint256 prefund, address actualAggregator, bytes memory sigForUserOp, bytes memory sigForAggregation, bytes memory offChainSigInfo) { + +struct UserOpsPerAggregator { + UserOperation[] userOps; + IAggregator aggregator; + bytes signature; +} ``` The core interface required for a wallet to have is: -```c++ -function validateUserOp - (UserOperation calldata userOp, bytes32 requestId, uint256 missingWalletFunds) - external; +```solidity +interface IWallet { + function validateUserOp + (UserOperation calldata userOp, bytes32 requestId, address aggregator, uint256 missingWalletFunds) + external; +} +``` +The wallet +* MUST validate the caller is a trusted EntryPoint +* The requestId is a hash over the userOp (except signature), entryPoint and chainId +* If the wallet does not support signature aggregation, it MUST validate the signature is a valid signature of the `requestId` +* MUST pay the entryPoint (caller) at least the "missingWalletFunds" (which might be zero, in case current wallet's deposit is high enough) +* The wallet MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it) +* The `aggregator` SHOULD be ignored for wallets that don't use an aggregator + +A Wallet that works with aggregated signature should have the interface: +```solidity +interface IAggregatedWallet is IWallet { + + function getAggregator() view returns (address); +} +``` +* **getAggregator()** returns the aggregator this wallet supports. +* **validateUserOp()** (inherited from IWallet interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator` +* The wallet should also support aggregator-specific getter (e.g. `getAggregationInfo()`). + This method should export the wallet's public-key to the aggregator, and possibly more info + (note that it is not called directly by the entryPoint) +* validateUserOp MAY ignore the signature field + (the signature might contain data extracted by the `aggregator.validateUserOpSignature`, and used by `aggregator.validateSignatures`) + +The core interface required by an aggregator is: +```solidity +interface IAggregator { + + function validateUserOpSignature(UserOperation calldata userOp, bool offChainSigCheck) external view returns (bytes memory sigForUserOp, bytes memory sigForAggregation, bytes memory offChainSigInfo); + + function aggregateSignatures(bytes[] calldata sigsForAggregation) external view returns (bytes memory aggregatesSignature); + + function validateSignatures(UserOperation[] calldata userOps, bytes calldata signature) view external; +} ``` +* **validateUserOpSignature()** must validate the userOp's signature against the UserOp's hash (the same hash MUST be used for + **validateUserOpSignature** and later for **validateSignatures**) +* it is called (as an off-chain view call) from `simulateValidation()`. +* The method should return a replacement value for the UserOp.signature, and a value used for aggregation + (the trivial "split" is return "" for the sigForUserOp, and the signature itself for sigForAggregation) +* if the **offChainSigCheck** param is true, it should not validate the signature, and instead return **offChainSigInfo**, + which is used by a companion off-chain library code to validate the signature +* **aggregateSignatures()** must aggregate all "sigForAggregation" into a single value. + This method is a helper method for the bundler. The bundler MAY use a native library to perform the signature aggregation +* **validateSignatures()** MUST validate the aggregated signature matches for all UserOperations in the array, and revert otherwise. + This method is called on-chain by `handleOps()` + +#### Trusting aggregators and off-chain optimization + +The aggregators SHOULD stake just like a paymaster. Bundlers MAY throttle down and ban aggregators in case they take too much +resources (or revert) when the above methods are called in view mode. +Alternately, bundlers, MAY "whitelist" specific implementations of aggregators. +Blocking userOp with a banned aggregator is done during [Simulation](#simulation) +The aggregator of a given wallet is exposed by the `getAggregator()` method. +In case the UserOp creates a new wallet, the aggregator is returned by the `simulateValidation()` call. + +To use an off-chain optimized implementation, the bundler MAY: +* call `simulateValidation()` with **offChainSigCheck=true** +* validate the response to have the correct aggregator. +* call off-chain library to validate the signature +* In case the aggregator is trusted, but doesn't support off-chain optimization, the bundler MAY + repeat the simulateValidation, or just call the aggregator.validateUserOpSignature (as a view call) to complete the validation. + ### Required entry point contract functionality +There are 2 separate entry point methods: `handleOps` and `handleAggregatedOps` +* `handleOps` handle userOps of wallets that don't require any signature aggregator. +* `handleAggregatedOps` can handle a batch that contains userOps of multiple aggregators (and also requests without any aggregator) +* `handleAggregatedOps` performs the same logic below as `handleOps`, but it must transfer the correct aggregator to each userOp, and also must call `validateSignatures` on each aggregator after doing all the per-wallet validation. The entry point's `handleOps` function must perform the following steps (we first describe the simpler non-paymaster case). It must make two loops, the **verification loop** and the **execution loop**. In the verification loop, the `handleOps` call must perform the following steps for each `UserOperation`: -* **Create the wallet if it does not yet exist**, using the initcode provided in the `UserOperation`. If the wallet does not exist, _and_ the initcode is empty, or the newly deployed contract address differs from `UserOperation.sender`, the call must fail. -* **Call `validateUserOp` on the wallet**, passing in the `UserOperation` and the required fee. The wallet should verify the operation's signature, and pay the fee if the wallet considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely. +* **Create the wallet if it does not yet exist**, using the initcode provided in the `UserOperation`. If the wallet does not exist, _and_ the initcode is empty, or does not deploy a contract at the "sender" address, the call must fail. +* **Call `validateUserOp` on the wallet**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The wallet should verify the operation's signature, and pay the fee if the wallet considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely. +* Validate the wallet's deposit in the entryPoint is high enough to cover the max possible cost (cover the already-done verification and max execution gas) In the execution loop, the `handleOps` call must perform the following steps for each `UserOperation`: -* **Call the wallet with the `UserOperation`'s calldata**. It's up to the wallet to choose how to parse the calldata; an expected worlflow is for the wallet to have an `execute` function that parses the remaining calldata as a series of one or more calls that the wallet should make. -* **Refund unused gas fees** to the wallet +* **Call the wallet with the `UserOperation`'s calldata**. It's up to the wallet to choose how to parse the calldata; an expected workflow is for the wallet to have an `execute` function that parses the remaining calldata as a series of one or more calls that the wallet should make. ![](../assets/eip-4337/image1.png) -Before accepting a `UserOperation`, bundlers must use an RPC method to locally simulate calling the `simulateValidation` function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the [Simulation section below](#simulation) for details. +Before accepting a `UserOperation`, bundlers should use an RPC method to locally call the `simulateValidation` function of the entry point, to verify that the signature is correct and the operation actually pays fees; see the [Simulation section below](#simulation) for details. +A node/bundler SHOULD drop (and not add to the mempool) `UserOperation` that fails the validation ### Extension: paymasters -We extend the entry point logic to support **paymasters** that can sponsor transactions for other users. This feature can be used to allow application developers to subsidize fees for their users, allow users to pay fees with ERC-20 tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow: +We extend the entry point logic to support **paymasters** that can sponsor transactions for other users. This feature can be used to allow application developers to subsidize fees for their users, allow users to pay fees with EIP-20 tokens and many other use cases. When the paymaster is not equal to the zero address, the entry point implements a different flow: ![](../assets/eip-4337/image2.png) -During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster is staked, and also has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. Additionally, the `validateUserOp` must be called with a `requiredPrefund` of 0 to reflect that it's the paymaster, and not the wallet, that's paying the fees. +During the verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster is staked, and also has enough ETH deposited with the entry point to pay for the operation, and then call `validatePaymasterUserOp` on the paymaster to verify that the paymaster is willing to pay for the operation. Note that in this case, the `validateUserOp` is called with a `missingWalletFunds` of 0 to reflect that the wallet's deposit is not used for payment for this userOp. During the execution loop, the `handleOps` execution must call `postOp` on the paymaster after making the main execution call. It must guarantee the execution of `postOp`, by making the main execution inside an inner call context, and if the inner call context reverts attempting to call `postOp` again in an outer call context. @@ -107,7 +195,7 @@ The paymaster interface is as follows: ```c++ function validatePaymasterUserOp (UserOperation calldata userOp, bytes32 requestId, uint256 maxCost) - external view returns (bytes memory context); + external returns (bytes memory context); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost) @@ -152,7 +240,7 @@ function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) ext When a client receives a `UserOperation`, it must first run some basic sanity checks, namely that: - Either the `sender` is an existing contract, or the `initCode` is not empty (but not both) -- The `verificationGas` is sufficiently low (`<= MAX_VERIFICATION_GAS`) and the `preVerificationGas` is sufficiently high (enough to pay for the calldata gas cost of serializing the `UserOperation` plus `PRE_VERIFICATION_OVERHEAD_GAS`) +- The `verificationGasLimit` is sufficiently low (`<= MAX_VERIFICATION_GAS`) and the `preVerificationGas` is sufficiently high (enough to pay for the calldata gas cost of serializing the `UserOperation` plus `PRE_VERIFICATION_OVERHEAD_GAS`) - The paymaster is either the zero address or is a contract which (i) currently has nonempty code on chain, (ii) has registered and staked, (iii) has a sufficient deposit to pay for the UserOperation, and (iv) is not currently banned. - The callgas is at least the cost of a `CALL` with non-zero value. - The `maxFeePerGas` and `maxPriorityFeePerGas` are above a configurable minimum value that the client is willing to accept. At the minimum, they are sufficiently high to be included with the current `block.basefee`. @@ -162,31 +250,32 @@ If the `UserOperation` object passes these sanity checks, the client must next r ### Simulation -To simulate a `UserOperation` `op` validation, the client makes an `eth_call` with the following params: +To simulate a `UserOperation` validation, the client makes a view call to `simulateValidation(userop)`, with a "from" address set to all-zeros -```python -{ - "from": 0x0000000000000000000000000000000000000000, - "to": [entry point address], - "input": [simulateValidation header] + serialize(op), -} -``` +If the call returns an error, the client rejects this `userOp`. -If the call returns an error, the client rejects the `op`. +The simulated call performs the full validation, by +calling: +1. `wallet.validateUserOp`. +2. if specified a paymaster: `paymaster.validatePaymasterUserOp`. +3. if using an aggregator: `aggregator.validateUserOpSignature`. -The simulated call performs the full validation, calling both `wallet.validateUserOp` and (if specified) `paymaster.validatePaymasterUserOp`. -The two operations differ in their opcode banning policy. -In order to distinguish between the two, there is a single call to the NUMBER opcode (`block.number`), used as a delimiter between wallet validation restrictions and paymaster validation restrictions. -While simulating `op` validation, the client should make sure that: +The operations differ in their opcode banning policy. +In order to distinguish between them, there is a call to the NUMBER opcode (`block.number`), used as a delimiter between the validation functions. +While simulating `userOp` validation, the client should make sure that: 1. Neither call's execution trace invokes any **forbidden opcodes** -2. The first call does not access _mutable state_ of any contract except the wallet itself and its deposit in the entry point contract. _Mutable state_ definition includes both storage and balance. -3. The second call does not access _mutable state_ of any contract except the paymaster itself. -4. Any `CALL` or `CALLCODE` during validation has `value=0`, except for the transfer from the wallet to the entry point. -5. No `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` results in an out-of-gas revert. -6. Any `GAS` opcode is followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }. -7. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op. -8. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call, otherwise forbid `CREATE2`. +2. The first (validateUserOp) call does not access _mutable state_ of any contract except the wallet itself and its deposit in the entry point contract. _Mutable state_ definition includes both storage and balance. +3. The second (validatePaymasterUserOp) call does not access _mutable state_ of any contract except the paymaster itself. The paymaster is also not allowed to make any state-change calls (SSTORE) + A bundler MAY whitelist specific paymaster addresses, and not enforce the above storage limitations +4. The third (validateUserOpSignature) view-call doesn't access _mutable state_ of any contract except the aggregator AND wallet itself. +5. The third (validateUserOpSignature) is the first view call after the "NUMBER" marker. If that aggregator is banned, then this staticcall MUST revert immediately, as if it was called with zero gas. +6. Any `CALL` or `CALLCODE` during validation has `value=0`, except for the transfer from the wallet to the entry point. +7. No `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` results in an out-of-gas revert. +8. No `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` to addresses with `EXTCODESIZE=0`. +9. Any `GAS` opcode is followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }. +10. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op. +11. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call (in the validateUserOp block), otherwise forbid `CREATE2`. Since the wallet is allowed to access its own entry point deposit in order to top it up when needed, the client must know the storage slot in order to whitelist it. The entry point therefore implements the following view function: @@ -194,10 +283,13 @@ Since the wallet is allowed to access its own entry point deposit in order to to function getSenderStorage(address sender) external view returns (uint256[] memory senderStorageCells) ``` +### Bundling -During batching, the client should: +During bundling, the client should: - Exclude UserOps that access any sender address created by another UserOp on the same batch (via CREATE2 factory). - For each paymaster used in the batch, keep track of the balance while adding UserOps. Ensure that it has sufficient deposit to pay for all the UserOps that use it. +- Sort UserOps by aggregator, to create the lists of UserOps-per-aggregator. +- For each aggregator, run the aggregator-specific code to create aggregated signature, and update the UserOps After creating the batch, before including the transaction in a block, the client should: - Run `eth_estimateGas` with maximum possible gas, to verify the entire `handleOps` batch transaction, and use the estimated gas for the actual transaction execution. @@ -205,7 +297,7 @@ After creating the batch, before including the transaction in a block, the clien In practice, restrictions (2) and (3) basically mean that the only external accesses that the wallet and the paymaster can make are reading code of other contracts if their code is guaranteed to be immutable (eg. this is useful for calling or delegatecalling to libraries). -If any of the three conditions is violated, the client should reject the `op`. If both calls succeed (or, if `op.paymaster == ZERO_ADDRESS` and the first call succeeds) without violating the three conditions, the client should accept the op. On a bundler node, the storage keys accessed by both calls must be saved as the `accessList` of the `UserOperation` +If any of the three conditions is violated, the client should reject the `op`. If both calls succeed (or, if `op.paymaster == ZERO_ADDRESS` and the first call succeeds)without violating the three conditions, the client should accept the op. On a bundler node, the storage keys accessed by both calls must be saved as the `accessList` of the `UserOperation` When a bundler includes a bundle in a block it must ensure that earlier transactions in the block don't make any UserOperation fail. It should either use access lists to prevent conflicts, or place the bundle as the first transaction in the block. @@ -216,6 +308,7 @@ The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the Exceptions to the forbidden opcodes: 1. A single `CREATE2` is allowed if `op.initcode.length != 0` and must result in the deployment of a previously-undeployed `UserOperation.sender`. 2. `GAS` is allowed if followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }. + (that is, making calls is allowed, using `gasleft()` or `gas` opcode directly is forbidden) ### Reputation scoring and throttling/banning for paymasters @@ -281,13 +374,12 @@ The result `SHOULD` be set to true if and only if the request passed simulation nonce, // uint256 initCode, // bytes callData, // bytes - callGas, // uint256 - verificationGas, // uint256 + callGasLimit, // uint256 + verificationGasLimit, // uint256 preVerificationGas, // uint256 maxFeePerGas, // uint256 maxPriorityFeePerGas, // uint256 - paymaster, // address - paymasterData, // bytes + paymasterAndData, // bytes signature // bytes }, entryPoint // address @@ -330,7 +422,7 @@ eth_supportedEntryPoints returns an array of the entryPoint addresses supported The main challenge with a purely smart contract wallet based account abstraction system is DoS safety: how can a miner including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the miner to execute the entire operation opens a DoS attack vector, as an attacker could easily send many operations that pretend to pay a fee but then revert at the last moment after a long execution. Similarly, to prevent attackers from cheaply clogging the mempool, nodes in the P2P network need to check if an operation will pay a fee before they are willing to forward it. -In this proposal, we expect wallets to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the wallet itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the wallet, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGas` of the `UserOperation`; nodes can choose to reject operations whose `verificationGas` is too high. These restrictions allow miners and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block. +In this proposal, we expect wallets to have a `validateUserOp` method that takes as input a `UserOperation`, and verify the signature and pay the fee. This method is required to be almost-pure: it is only allowed to access the storage of the wallet itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the wallet, and can also send out ETH (needed to pay the entry point). The method is gas-limited by the `verificationGasLimit` of the `UserOperation`; nodes can choose to reject operations whose `verificationGasLimit` is too high. These restrictions allow miners and network nodes to simulate the verification step locally, and be confident that the result will match the result when the operation actually gets included into a block. The entry point-based approach allows for a clean separation between verification and execution, and keeps wallets' logic simple. The alternative would be to require wallets to follow a template where they first self-call to verify and then self-call to execute (so that the execution is sandboxed and cannot cause the fee payment to revert); template-based approaches were rejected due to being harder to implement, as existing code compilation and verification tooling is not designed around template verification. @@ -338,17 +430,28 @@ The entry point-based approach allows for a clean separation between verificatio Paymasters facilitate transaction sponsorship, allowing third-party-designed mechanisms to pay for transactions. Many of these mechanisms _could_ be done by having the paymaster wrap a `UserOperation` with their own, but there are some important fundamental limitations to that approach: -* No possibility for "passive" paymasters (eg. that accept fees in some ERC-20 token at an exchange rate pulled from an on-chain DEX) +* No possibility for "passive" paymasters (eg. that accept fees in some EIP-20 token at an exchange rate pulled from an on-chain DEX) * Paymasters run the risk of getting griefed, as users could send ops that appear to pay the paymaster but then change their behavior after a block -The paymaster scheme allows a contract to passively pay on users' behalf under arbitrary conditions. It even allows ERC-20 token paymasters to secure a guarantee that they would only need to pay if the user pays them: the paymaster contract can check that there is sufficient approved ERC-20 balance in the `validatePaymasterUserOp` method, and then extract it with `transferFrom` in the `postOp` call; if the op itself transfers out or de-approves too much of the ERC-20s, the inner `postOp` will fail and revert the execution and the outer `postOp` can extract payment (note that because of storage access restrictions the ERC-20 would need to be a wrapper defined within the paymaster itself). +The paymaster scheme allows a contract to passively pay on users' behalf under arbitrary conditions. It even allows EIP-20 token paymasters to secure a guarantee that they would only need to pay if the user pays them: the paymaster contract can check that there is sufficient approved EIP-20 balance in the `validatePaymasterUserOp` method, and then extract it with `transferFrom` in the `postOp` call; if the op itself transfers out or de-approves too much of the EIP-20s, the inner `postOp` will fail and revert the execution and the outer `postOp` can extract payment (note that because of storage access restrictions the EIP-20 would need to be a wrapper defined within the paymaster itself). ### First-time wallet creation It is an important design goal of this proposal to replicate the key property of EOAs that users do not need to perform some custom action or rely on an existing user to create their wallet; they can simply generate an address locally and immediately start accepting funds. -This is accomplished by having the entry point itself create wallets using CREATE2. The `UserOperation` struct has an `initCode` field; this field would be empty for all operations by a given wallet after the first, but the first operation would fill in the `initCode`. The entry point uses [EIP-2470](./eip-2470.md) deployer contract to create the wallet, and then performs the operation. The user can compute the address of their wallet by locally running the [EIP 1014](./eip-1014.md) CREATE2 address formula. The salt used is the `nonce` of the `UserOperation`. -(The entry point contract has a utility method `getSenderAddress()` for that purpose) +The wallet creation itself is done by a "factory" contract, with wallet-specific data. +The factory is expected to use CREATE2 (not CREATE) to create the wallet, so that the order of creation of wallets doesn't interfere with the generated addresses. +The `initCode` field (if non-zero length) is parsed as a 20-byte address, followed by "calldata" to pass to this address. +This method call is expected to create a wallet and return its address. +When `initCode` is specified, if either the `sender` address points to an existing contract, or (after calling the initCode) the `sender` address still does not exist, +then the operation is aborted. +The `initCode` MUST NOT be called directly from the entryPoint, but from another address. +The contract created by this factory method should accept a call to `validateUserOp` to validate the UserOp's signature. +For security reasons, it is important that the generated contract address will depend on the initial signature. +This way, even if someone can create a wallet at that address, he can't set different credentials to control it. + +NOTE: In order for the wallet to determine the "counterfactual" address of the wallet (prior its creation), +it should make a static call to the `entryPoint.createSender()` ### Entry point upgrading @@ -356,15 +459,15 @@ Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficienc ## Backwards Compatibility -This ERC does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-ERC-4337 wallets, because those wallets do not have a `validateUserOp` function. If the wallet has a function for authorizing a trusted op submitter, then this could be fixed by creating an ERC-4337-compatible wallet that re-implements the verification logic as a wrapper and setting it to be the original wallet's trusted op submitter. +This EIP does not change the consensus layer, so there are no backwards compatibility issues for Ethereum as a whole. Unfortunately it is not easily compatible with pre-4337 wallets, because those wallets do not have a `validateUserOp` function. If the wallet has a function for authorizing a trusted op submitter, then this could be fixed by creating an 4337-compatible wallet that re-implements the verification logic as a wrapper and setting it to be the original wallet's trusted op submitter. ## Reference Implementation -See https://github.com/eth-infinitism/account-abstraction/tree/audit +See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts` -## Security considerations +## Security Considerations -The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ ERC 4337 wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust. +The entry point contract will need to be very heavily audited and formally verified, because it will serve as a central trust point for _all_ [EIP-4337](./eip-4337.md) wallets. In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _wallets_ have to do becomes much smaller (they need only verify the `validateUserOp` function and its "check signature, increment nonce and pay fees" logic) and check that other functions are `msg.sender == ENTRY_POINT` gated (perhaps also allowing `msg.sender == self`), but it is nevertheless the case that this is done precisely by concentrating security risk in the entry point contract that needs to be verified to be very robust. Verification would need to cover two primary claims (not including claims needed to protect paymasters, and claims needed to establish p2p-level DoS resistance): @@ -373,3 +476,4 @@ Verification would need to cover two primary claims (not including claims needed ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). + diff --git a/EIPS/eip-4519.md b/EIPS/eip-4519.md index ee30f7bab8c4ce..b852d446304750 100644 --- a/EIPS/eip-4519.md +++ b/EIPS/eip-4519.md @@ -12,22 +12,22 @@ requires: 165, 721 --- ## Abstract -This EIP proposes a standard interface for non-fungible tokens that represent physical assets, such as Internet of Things (IoT) devices. A SmartNFT is tied to a physical asset that can check if the tie is authentic or not. The SmartNFT can include an Ethereum address of the physical asset and, consequently, the physical asset can sign messages or transactions. The physical asset can operate with an operating mode that is defined by its SmartNFT with an attribute named state. The token state can define if the token owner or the token user can use the asset or not. A cryptographically secure mutual authentication process can be carried out between the physical asset and its owner or its user. SmartNFTs extend [ERC-721](./eip-721.md) non-fungible tokens, which only allow representing assets by a unique identifier, but not by an Ethereum address. Moreover, SmartNFTs extend ERC-721 NFTs to include users in addition to owners. +This EIP proposes a standard interface for non-fungible tokens that represent physical assets, such as Internet of Things (IoT) devices. An [EIP-4519](./eip-4519.md) NFT is tied to a physical asset that can check if the tie is authentic or not. The EIP-4519 NFT can include an Ethereum address of the physical asset and, consequently, the physical asset can sign messages or transactions. The physical asset can operate with an operating mode that is defined by its EIP-4519 NFT with an attribute named state. The token state can define if the token owner or the token user can use the asset or not. A cryptographically secure mutual authentication process can be carried out between the physical asset and its owner or its user. EIP-4519 NFTs extend [EIP-721](./eip-721.md) non-fungible tokens, which only allow representing assets by a unique identifier, but not by an Ethereum address. Moreover, EIP-4519 NFTs extend EIP-721 NFTs to include users in addition to owners. ## Motivation -This SmartNFT was developed because the ERC-721 NFT does not include the users of an asset (only include the owners) and does not include the Ethereum address of the asset. Smart assets (for example, IoT devices) are increasing nowadays. If smart assets are tied to SmartNFTs then they can be managed in a secure and traceable way. The reason is that SmartNFTs, unlike ERC-721 NFTs, allow establishing secure communication channels between the physical asset and its owner and its user. In this way, assets, owners and users can be assured of exchanging information with trusted parties. +This EIP-4519 NFT was developed because the EIP-721 NFT does not include the users of an asset (only include the owners) and does not include the Ethereum address of the asset. Smart assets (for example, IoT devices) are increasing nowadays. If smart assets are tied to EIP-4519 NFTs then they can be managed in a secure and traceable way. The reason is that EIP-4519 NFTs, unlike EIP-721 NFTs, allow establishing secure communication channels between the physical asset and its owner and its user. In this way, assets, owners and users can be assured of exchanging information with trusted parties. -**Secure Physical Asset Tied to a SmartNFT** +**Secure Physical Asset Tied to an EIP-4519 NFT** Current non-fungible tokens are associated with passive assets, either virtual or physical things, but they do not include any standardized mechanism to tie the non-fungible token to the asset. Tying physical assets to NFTs is interesting because the asset can know anytime its owner, user, operating mode, and how to establish secure communication channels with its owner and user. The assets, owners and users are identified by their Ethereum addresses and the Ethereum address of the asset can be obtained from a unique physical property of the physical asset (for example, using a physical unclonable function). The asset can be an active part in any transfer of ownership and use. In addition, the asset is smart, for example to not obey orders from a non-authorized user, or to be inoperative if a successful authentication with the user or the owner has not been fulfilled. **User Management Mechanism** -SmartNFTs allow implementing a new and useful user management mechanism. In the last few years, many projects concerning assets sharing (for example, vehicles) have been created and developed. SmartNFTs incorporate the Ethereum address of the user as another attribute of the token in order to distinguish between the user, who employs the asset for a specific application, and the owner, who assigns the token to users. Hence, both users and owners of an asset can be traced. +EIP-4519 NFTs allow implementing a new and useful user management mechanism. In the last few years, many projects concerning assets sharing (for example, vehicles) have been created and developed. EIP-4519 NFTs incorporate the Ethereum address of the user as another attribute of the token in order to distinguish between the user, who employs the asset for a specific application, and the owner, who assigns the token to users. Hence, both users and owners of an asset can be traced. **Secure Key Exchange Mechanism** The engagement of the asset with an owner or a user is carried out after a mutual authentication protocol (for example, based on elliptic curve Diffie-Hellman key exchange protocol). This protocol can be employed for a key agreement between the asset and its owner, in the one side, and between the asset and its user, in the other side. ## Specification -The SmartNFT attributes `addressAsset` and `addressUser` are, respectively, the Ethereum addresses of the physical asset and the user. They are optional attributes but at least one of them should be used in a SmartNFT. In the case of using only the attribute `addressUser`, two states define if the token is assigned or not to a user. `Figure 1` shows these states in a flow chart. When a token is created, transferred or unassigned, the token state is set to `notAssigned`. If the token is assigned to a valid user, the state is set to `userAssigned`. +The EIP-4519 NFT attributes `addressAsset` and `addressUser` are, respectively, the Ethereum addresses of the physical asset and the user. They are optional attributes but at least one of them should be used in an EIP-4519 NFT. In the case of using only the attribute `addressUser`, two states define if the token is assigned or not to a user. `Figure 1` shows these states in a flow chart. When a token is created, transferred or unassigned, the token state is set to `notAssigned`. If the token is assigned to a valid user, the state is set to `userAssigned`. ![Figure 1 : Flow chart of the token states with `addressUser` defined (and `addressAsset` undefined)](../assets/eip-4519/images/Figure1.jpg) @@ -35,17 +35,17 @@ In the case of defining the attribute `addressAsset` but not the attribute `addr ![Figure 2 : Flow chart of the token states with `addressAsset` defined (and `addressUser` undefined)](../assets/eip-4519/images/Figure2.jpg) -Finally, if both the attributes `addressAsset` and `addressUser` are defined, the states of the SmartNFT define if the asset has been engaged or not with the owner or the user (`waitingForOwner`, `engagedWithOwner`, `waitingForUser` and `engagedWithUser`). The flow chart in `Figure 3` shows all the possible state changes. The states related to the owner are the same as in `Figure 2`. The difference is that, at the state `engagedWithOwner`, the token can be assigned to a user. If a user is assigned (the token being at states `engagedWithOwner`, `waitingForUser` or `engagedWithUser`), then the token changes its state to `waitingForUser`. Once the asset and the user authenticate each other, the state of the token is set to `engagedWithUser`, and the user is able to use the asset. +Finally, if both the attributes `addressAsset` and `addressUser` are defined, the states of the EIP-4519 NFT define if the asset has been engaged or not with the owner or the user (`waitingForOwner`, `engagedWithOwner`, `waitingForUser` and `engagedWithUser`). The flow chart in `Figure 3` shows all the possible state changes. The states related to the owner are the same as in `Figure 2`. The difference is that, at the state `engagedWithOwner`, the token can be assigned to a user. If a user is assigned (the token being at states `engagedWithOwner`, `waitingForUser` or `engagedWithUser`), then the token changes its state to `waitingForUser`. Once the asset and the user authenticate each other, the state of the token is set to `engagedWithUser`, and the user is able to use the asset. ![Figure 3 : Flow chart of the token states with `addressUser` and `addressUser` defined](../assets/eip-4519/images/Figure3.jpg) -In order to complete the ownership transfer of a token, the new owner must carry out a mutual authentication process with the asset, which is off-chain with the asset and on-chain with the token, by using their Ethereum addresses. Similarly, a new user must carry out a mutual authentication process with the asset to complete a use transfer. SmartNFTs define how the authentication processes start and finish. These authentication processes allow deriving fresh session cryptographic keys for secure communication between assets and owners, and between assets and users. Therefore, the trustworthiness of the assets can be traced even if new owners and users manage them. +In order to complete the ownership transfer of a token, the new owner must carry out a mutual authentication process with the asset, which is off-chain with the asset and on-chain with the token, by using their Ethereum addresses. Similarly, a new user must carry out a mutual authentication process with the asset to complete a use transfer. EIP-4519 NFTs define how the authentication processes start and finish. These authentication processes allow deriving fresh session cryptographic keys for secure communication between assets and owners, and between assets and users. Therefore, the trustworthiness of the assets can be traced even if new owners and users manage them. -When the SmartNFT is created or when the ownership is transferred, the token state is `waitingForOwner`. Assuming that the asset is an electronic physical asset, it saves in its memory the owner address and sets its operating mode to `waitingForOwner`. The owner generates a pair of keys using the elliptic curve secp256k1 and the primitive element P used on this curve: a secret key SKO_A and a Public Key PKO_A, so that PKO_A = SKO_A*P. To generate the shared key between the owner and the asset, KO, the public key of the asset, PKA, is employed as follows: +When the EIP-4519 NFT is created or when the ownership is transferred, the token state is `waitingForOwner`. The asset sets its operating mode to `waitingForOwner`. The owner generates a pair of keys using the elliptic curve secp256k1 and the primitive element P used on this curve: a secret key SKO_A and a Public Key PKO_A, so that PKO_A = SKO_A*P. To generate the shared key between the owner and the asset, KO, the public key of the asset, PKA, is employed as follows: KO=PKA*SKO_A -Using the function `startOwnerEngagement`, the owner saves PKO_A as the attribute `dataEngagement` and the hash of KO as the attribute `hashK_OA` in the SmartNFT. The owner sends PKO_A signed to the asset. The asset checks the signature of the owner and, if signature is correct, the asset uses PKO_A to calculate: +Using the function `startOwnerEngagement`, PKO_A is saved as the attribute `dataEngagement` and the hash of KO as the attribute `hashK_OA`. The owner sends request engagement to the asset, and the asset calculates: KA = SKA*PKO_A @@ -57,9 +57,9 @@ Using the function `ownerEngagement`, the asset sends the hash of KA ![Figure 4: Steps in a successful owner and asset mutual authentication process](../assets/eip-4519/images/Figure4.jpg) -If the asset consults Ethereum and the state of its SmartNFT is `waitingForUser`, the asset (assuming it is an electronic physical asset) saves in its memory the user address and sets its operating mode to `waitingForUser`. Then, a mutual authentication process is carried out with the user, as already done with the owner. The user sends the transaction associated with the function `startUserEngagement`. As in `startOwnerEngagement`, this function saves the public key generated by the user, PKU_A, as the attribute `dataEngagement` and the hash of KU=PKA*SKU_A as the attribute `hashK_UA` in the SmartNFT. +If the asset consults Ethereum and the state of its EIP-4519 NFT is `waitingForUser`, the asset (assuming it is an electronic physical asset) sets its operating mode to `waitingForUser`. Then, a mutual authentication process is carried out with the user, as already done with the owner. The user sends the transaction associated with the function `startUserEngagement`. As in `startOwnerEngagement`, this function saves the public key generated by the user, PKU_A, as the attribute `dataEngagement` and the hash of KU=PKA*SKU_A as the attribute `hashK_UA` in the EIP-4519 NFT. -The user sends PKU_A signed to the asset. The latter checks the signature of the user and, if signature is correct, the asset uses PKU_A to calculate: +The user sends request engagement and the asset calculates: KA = SKA*PKU_A @@ -69,16 +69,16 @@ KU=PKA\*SKU_A=(SKA\*P)\*SKU_AA obtained and if it is the same as the data in `hashK_UA`, then the state of the token changes to `engagedWithUser` and the event `UserEngaged` is sent. Once the asset receives the event, it changes its operation mode to `engagedWithUser`. This process is shown in `Figure 5`. From this moment, the asset can be managed by the user and they can communicate in a secure way using the shared key. - ![Figure 5: Steps in a successful user and asset mutual authentication process](../assets/eip-4519/images/Fig5_rev.png) + ![Figure 5: Steps in a successful user and asset mutual authentication process](../assets/eip-4519/images/Figure5.jpg) -Since the establishment of a shared secret key is very important for a secure communication, SmartNFTs include the attributes +Since the establishment of a shared secret key is very important for a secure communication, EIP-4519 NFTs include the attributes `hashK_OA`, `hashK_UA` and `dataEngagement`. The first two attributes define, respectively, the hash of the secret key shared between the asset and its owner and between the asset and its user. Assets, owners and users should check they are using the correct shared secret keys. The attribute `dataEngagement` defines the public data needed for the agreement. ```solidity pragma solidity ^0.8.0; - /// @title SmartNFT: Extension of ERC-721 Non-Fungible Token Standard. -/// Note: the ERC-165 identifier for this interface is 0x8a68abe3 - interface SmartNFT is ERC721/*,ERC165*/{ + /// @title EIP-4519 NFT: Extension of EIP-721 Non-Fungible Token Standard. +/// Note: the EIP-165 identifier for this interface is 0x8a68abe3 + interface EIP-4519 NFT is EIP721/*,EIP165*/{ /// @dev This emits when the NFT is assigned as utility of a new user. /// This event emits when the user of the token changes. /// (`_addressUser` == 0) when no user is assigned. @@ -93,21 +93,21 @@ pragma solidity ^0.8.0; event OwnerEngaged(uint256 indexed tokenId); /// @dev This emits when it is checked that the timeout has expired. - /// This event emits when the timestamp of the SmartNFT is not updated in timeout. + /// This event emits when the timestamp of the EIP-4519 NFT is not updated in timeout. event TimeoutAlarm(uint256 indexed tokenId); /// @notice This function defines how the NFT is assigned as utility of a new user (if "addressUser" is defined). - /// @dev Only the owner of the SmartNFT can assign a user. If "addressAsset" is defined, then the state of the token must be + /// @dev Only the owner of the EIP-4519 NFT can assign a user. If "addressAsset" is defined, then the state of the token must be /// "engagedWithOwner","waitingForUser" or "engagedWithUser" and this function changes the state of the token defined by "_tokenId" to /// "waitingForUser". If "addressAsset" is not defined, the state is set to "userAssigned". In both cases, this function sets the parameter /// "addressUser" to "_addressUser". - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @param _addressUser is the address of the new user. function setUser(uint256 _tokenId, address _addressUser) external payable; /// @notice This function defines the initialization of the mutual authentication process between the owner and the asset. /// @dev Only the owner of the token can start this authentication process if "addressAsset" is defined and the state of the token is "waitingForOwner". /// The function does not change the state of the token and saves "_dataEngagement" /// and "_hashK_OA" in the parameters of the token. - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @param _dataEngagement is the public data proposed by the owner for the agreement of the shared key. /// @param _hashK_OA is the hash of the secret proposed by the owner to share with the asset. function startOwnerEngagement(uint256 _tokenId, uint256 _dataEngagement, uint256 _hashK_OA) external payable; @@ -124,14 +124,14 @@ pragma solidity ^0.8.0; /// @dev Only the user of the token can start this authentication process if "addressAsset" and "addressUser" are defined and /// the state of the token is "waitingForUser". The function does not change the state of the token and saves "_dataEngagement" /// and "_hashK_UA" in the parameters of the token. - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @param _dataEngagement is the public data proposed by the user for the agreement of the shared key. /// @param _hashK_UA is the hash of the secret proposed by the user to share with the asset. function startUserEngagement(uint256 _tokenId, uint256 _dataEngagement, uint256 _hashK_UA) external payable; /// @notice This function completes the mutual authentication process between the user and the asset. /// @dev Only the asset tied to the token can finish this authentication process provided that the state of the token is - /// "waitingForUser" and dataEngagement if different from 0. This function compares hashK_UA saved in + /// "waitingForUser" and dataEngagement is different from 0. This function compares hashK_UA saved in /// the token with hashK_A. If they are equal then the state of the token changes to "engagedWithUser", dataEngagement is set to 0, /// and the event "UserEngaged" is emitted. /// @param _hashK_A is the hash of the secret generated by the asset to share with the user. @@ -140,14 +140,14 @@ pragma solidity ^0.8.0; /// @notice This function checks if the timeout has expired. /// @dev Everybody can call this function to check if the timeout has expired. The event "TimeoutAlarm" is emitted /// if the timeout has expired. - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @return true if timeout has expired and false in other case. function checkTimeout(uint256 _tokenId) external returns (bool); /// @notice This function sets the value of timeout. /// @dev Only the owner of the token can set this value provided that the state of the token is "engagedWithOwner", /// "waitingForUser" or "engagedWithUser". - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @param _timeout is the value to assign to timeout. function setTimeout(uint256 _tokenId, uint256 _timeout) external; @@ -169,7 +169,7 @@ pragma solidity ^0.8.0; /// @notice This function lets know the user of the token from its tokenId. /// @dev Everybody can call this function. The code executed only reads from Ethereum. - /// @param _tokenId is the tokenId of the SmartNFT tied to the asset. + /// @param _tokenId is the tokenId of the EIP-4519 NFT tied to the asset. /// @return user of the token from its _tokenId. function userOf(uint256 _tokenId) external view returns (address); @@ -195,10 +195,10 @@ pragma solidity ^0.8.0; ``` ## Rationale -The demand for SmartNFTs, which allow user management and a tie to a physical asset are growing (for example, in the context of the Internet of Things). Therefore, it is essential to establish a standard capable of including all these options working together or separately. The incorporation of an Ethereum address of the user or an Ethereum address of a physical asset to the SmartNFT is optional. However, it does not make sense that the SmartNFT does not include any of them because, in that case, the SmartNFT would be an ERC-721 token. Since some functions such as `startUserEngagement` are available only if both addresses are implemented, a single interface with all the options is proposed. +The demand for EIP-4519 NFTs, which allow user management and a tie to a physical asset are growing (for example, in the context of the Internet of Things). Therefore, it is essential to establish a standard capable of including all these options working together or separately. The incorporation of an Ethereum address of the user or an Ethereum address of a physical asset to the EIP-4519 NFT is optional. However, it does not make sense that the EIP-4519 NFT does not include any of them because, in that case, the EIP-4519 NFT would be an EIP-721 token. Since some functions such as `startUserEngagement` are available only if both addresses are implemented, a single interface with all the options is proposed. -**SmartNFT** -This EIP proposes a non-fungible token tied to a physical asset. The asset is able to generate an Ethereum address and authenticate its user and its owner. Hence, the asset can be considered as a smart asset associated with an NFT. If the asset and the token are regarded as one thing, we can talk about a SmartNFT. +**EIP-4519 NFT** +This EIP proposes a non-fungible token tied to a physical asset. The asset is able to generate an Ethereum address and authenticate its user and its owner. Hence, the asset can be considered as a smart asset associated with an NFT. If the asset and the token are regarded as one thing, we can talk about an EIP-4519 NFT. **Authentication** This EIP proposes using the smart contract to verify the mutual authentication process between the physical asset and the owner or the user by verifying the hash of a shared key. @@ -207,18 +207,18 @@ This EIP proposes using the smart contract to verify the mutual authentication p This EIP proposes including the attribute timestamp (to register in Ethereum the last time that the physical asset checked the tie with its token) and the attribute timeout (to register the maximum delay time established for the physical asset to prove again the tie). These attributes avoid that a malicious owner or user could use the asset endlessly. When the asset calls `updateTimestamp`, the smart contract must call `block.timestamp`, which provides current block timestamp as seconds since Unix epoch. For this reason, `timeout` must be provided in seconds. -**ERC-721-based** -[EIP-721](./eip-721.md) is the most commonly-used standard for generic NFTs. This EIP extends ERC-721 for backwards compatibility. +**EIP-721-based** +[EIP-721](./eip-721.md) is the most commonly-used standard for generic NFTs. This EIP extends EIP-721 for backwards compatibility. ## Backwards Compatibility -This standard is an extension of ERC-721. It is fully compatible with both of the commonly used optional extensions (`IERC721Metadata` and `IERC721Enumerable`) mentioned in the EIP-721 standard. +This standard is an extension of EIP-721. It is fully compatible with both of the commonly used optional extensions (`IERC721Metadata` and `IERC721Enumerable`) mentioned in the EIP-721 standard. ## Test Cases -The test cases presented in the paper shown below are available [here] (../assets/eip-4519/PoC_SmartNFT/README.md). +The test cases presented in the paper shown below are available [here](../assets/eip-4519/PoC_SmartNFT/README.md). ## Reference Implementation A first version was presented in a paper of the Special Issue **Security, Trust and Privacy in New Computing Environments** of **Sensors** journal of **MDPI** editorial. The paper, entitled [Secure Combination of IoT and Blockchain by Physically Binding IoT Devices to Smart Non-Fungible Tokens Using PUFs](../assets/eip-4519/sensors-21-03119.pdf), was written by the same authors of this EIP. ## Security Considerations -In this EIP, a generic system has been proposed for the creation of non-fungible tokens tied to physical assets. A generic point of view based on the improvements of the current ERC-721 NFT is provided, such as the implementation of the user management mechanism, which does not affect the token's ownership. The physical asset should have the ability to generate an Ethereum address from itself in a totally random way so that only the asset is able to know the secret from which the Ethereum address is generated. In this way, identity theft is avoided and the asset can be proven to be completely genuine. In order to ensure this, it is recommended that only the manufacturer of the asset has the ability to create its associated token. In the case of an IoT device, the device firmware will be unable to share and modify the secret. Instead of storing the secrets, it is recommended that assets reconstruct their secrets from non-sensitive information such as the helper data associated with Physical Unclonable Functions (PUFs). Although a secure key exchange protocol based on elliptic curves has been proposed, the token is open to coexist with other types of key exchange. +In this EIP, a generic system has been proposed for the creation of non-fungible tokens tied to physical assets. A generic point of view based on the improvements of the current EIP-721 NFT is provided, such as the implementation of the user management mechanism, which does not affect the token's ownership. The physical asset should have the ability to generate an Ethereum address from itself in a totally random way so that only the asset is able to know the secret from which the Ethereum address is generated. In this way, identity theft is avoided and the asset can be proven to be completely genuine. In order to ensure this, it is recommended that only the manufacturer of the asset has the ability to create its associated token. In the case of an IoT device, the device firmware will be unable to share and modify the secret. Instead of storing the secrets, it is recommended that assets reconstruct their secrets from non-sensitive information such as the helper data associated with Physical Unclonable Functions (PUFs). Although a secure key exchange protocol based on elliptic curves has been proposed, the token is open to coexist with other types of key exchange. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-4675.md b/EIPS/eip-4675.md index d8c87979fed4db..6e04d3f3ef53ed 100644 --- a/EIPS/eip-4675.md +++ b/EIPS/eip-4675.md @@ -209,4 +209,5 @@ If an arbitrary account has the right to call `setParentNFT()` there might be a To prevent this issue, implementors should just allow **admin** to call, or fractionalize and receive NFT in an atomic transaction similar to flash loan(swap). ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-4834.md b/EIPS/eip-4834.md index e0d6739d0c0250..d497c34b9a54b5 100644 --- a/EIPS/eip-4834.md +++ b/EIPS/eip-4834.md @@ -4,8 +4,7 @@ title: Hierarchical Domains description: Extremely generic name resolution author: Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/erc-4834-hierarchical-domains-standard/8388 -status: Last Call -last-call-deadline: 2022-09-30 +status: Review type: Standards Track category: ERC created: 2022-02-22 diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index d20624bea73fa1..05cb53b8528e49 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -13,7 +13,7 @@ requires: 1559, 2718, 2930 ## Abstract -Introduce a new transaction format for "blob-carrying transactions" which contain a large amount of data that cannot be +Introduce a new transaction format for "blob-carrying transactions" which contain a large amount of data that cannot be accessed by EVM execution, but whose commitment can be accessed. The format is intended to be fully compatible with the format that will be used in full sharding. @@ -24,7 +24,7 @@ Transaction fees on L1 have been very high for months and there is greater urgen Rollups are significantly reducing fees for many Ethereum users: Optimism and Arbitrum frequently provide fees that are ~3-8x lower than the Ethereum base layer itself, and ZK rollups, which have better data compression and can avoid including signatures, have fees ~40-100x lower than the base layer. -However, even these fees are too expensive for many users. The long-term solution to the long-term inadequacy of rollups +However, even these fees are too expensive for many users. The long-term solution to the long-term inadequacy of rollups by themselves has always been data sharding, which would add ~16 MB per block of dedicated data space to the chain that rollups could use. However, data sharding will still take a considerable amount of time to finish implementing and deploying. @@ -42,9 +42,6 @@ Compared to full data sharding, this EIP has a reduced cap on the number of thes | `BLOB_TX_TYPE` | `Bytes1(0x05)` | | `FIELD_ELEMENTS_PER_BLOB` | `4096` | | `BLS_MODULUS` | `52435875175126190479447740508185965837690552500527637822603658699938581184513` | -| `KZG_SETUP_G2` | `Vector[G2Point, FIELD_ELEMENTS_PER_BLOB]`, contents TBD | -| `KZG_SETUP_LAGRANGE` | `Vector[KZGCommitment, FIELD_ELEMENTS_PER_BLOB]`, contents TBD | -| `ROOTS_OF_UNITY` | `Vector[BLSFieldElement, FIELD_ELEMENTS_PER_BLOB]` | | `BLOB_COMMITMENT_VERSION_KZG` | `Bytes1(0x01)` | | `POINT_EVALUATION_PRECOMPILE_ADDRESS` | `Bytes20(0x14)` | | `POINT_EVALUATION_PRECOMPILE_GAS` | `50000` | @@ -72,80 +69,27 @@ Compared to full data sharding, this EIP has a reduced cap on the number of thes | `KZGCommitment` | `Bytes48` | Same as BLS standard "is valid pubkey" check but also allows `0x00..00` for point-at-infinity | | `KZGProof` | `Bytes48` | Same as for `KZGCommitment` | -### Helpers +### Cryptographic Helpers -Converts a blob to its corresponding KZG point: +Throughout this proposal we use cryptographic methods and classes defined in the corresponding [consensus 4844 specs](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844). -```python -def lincomb(points: List[KZGCommitment], scalars: List[BLSFieldElement]) -> KZGCommitment: - """ - BLS multiscalar multiplication. This function can be optimized using Pippenger's algorithm and variants. - """ - r = bls.Z1 - for x, a in zip(points, scalars): - r = bls.add(r, bls.multiply(x, a)) - return r - -def blob_to_kzg(blob: Blob) -> KZGCommitment: - return lincomb(KZG_SETUP_LAGRANGE, blob) -``` +Specifically, we use the following methods from [`polynomial-commitments.md`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/polynomial-commitments.md): +- [`verify_kzg_proof()`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/polynomial-commitments.md#verify_kzg_proof) +- [`evaluate_polynomial_in_evaluation_form()`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/polynomial-commitments.md#evaluate_polynomial_in_evaluation_form) -Converts a KZG point into a versioned hash: +We also use the following methods and classes from [`validator.md`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/validator.md): +- [`hash_to_bls_field()`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/validator.md#hash_to_bls_field) +- [`compute_powers()`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/validator.md#compute_powers) +- [`compute_aggregated_poly_and_commitment()`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/validator.md#compute_aggregated_poly_and_commitment) +- [`PolynomialAndCommitment`](https://github.com/ethereum/consensus-specs/blob/6c2b46ae3248760e0f6e52d61077d8b31e43ad1d/specs/eip4844/validator.md#PolynomialAndCommitment) + +### Helpers ```python def kzg_to_versioned_hash(kzg: KZGCommitment) -> VersionedHash: return BLOB_COMMITMENT_VERSION_KZG + hash(kzg)[1:] ``` -Verifies a KZG evaluation proof: - -```python -def verify_kzg_proof(polynomial_kzg: KZGCommitment, - x: BLSFieldElement, - y: BLSFieldElement, - quotient_kzg: KZGProof) -> bool: - # Verify: P - y = Q * (X - x) - X_minus_x = bls.add(KZG_SETUP_G2[1], bls.multiply(bls.G2, BLS_MODULUS - x)) - P_minus_y = bls.add(polynomial_kzg, bls.multiply(bls.G1, BLS_MODULUS - y)) - return bls.pairing_check([ - [P_minus_y, bls.neg(bls.G2)], - [quotient_kzg, X_minus_x] - ]) -``` - -Efficiently evaluates a polynomial in evaluation form using the barycentric formula - -```python -def bls_modular_inverse(x: BLSFieldElement) -> BLSFieldElement: - """ - Compute the modular inverse of x - i.e. return y such that x * y % BLS_MODULUS == 1 and return 0 for x == 0 - """ - return pow(x, -1, BLS_MODULUS) if x != 0 else 0 - - -def div(x, y): - """Divide two field elements: `x` by `y`""" - return x * bls_modular_inverse(y) % BLS_MODULUS - - -def evaluate_polynomial_in_evaluation_form(poly: List[BLSFieldElement], x: BLSFieldElement) -> BLSFieldElement: - """ - Evaluate a polynomial (in evaluation form) at an arbitrary point `x` - Uses the barycentric formula: - f(x) = (1 - x**WIDTH) / WIDTH * sum_(i=0)^WIDTH (f(DOMAIN[i]) * DOMAIN[i]) / (x - DOMAIN[i]) - """ - width = len(poly) - assert width == FIELD_ELEMENTS_PER_BLOB - inverse_width = bls_modular_inverse(width) - - for i in range(width): - r += div(poly[i] * ROOTS_OF_UNITY[i], (x - ROOTS_OF_UNITY[i]) ) - r = r * (pow(x, width, BLS_MODULUS) - 1) * inverse_width % BLS_MODULUS - - return r -``` - Approximates `2 ** (numerator / denominator)`, with the simplest possible approximation that is continuous and has a continuous derivative: ```python @@ -161,7 +105,7 @@ def fake_exponential(numerator: int, denominator: int) -> int: ### New transaction type We introduce a new [EIP-2718](./eip-2718.md) transaction type, -with the format being the single byte `BLOB_TX_TYPE` followed by an SSZ encoding of the +with the format being the single byte `BLOB_TX_TYPE` followed by an SSZ encoding of the `SignedBlobTransaction` container comprising the transaction contents: ```python @@ -173,7 +117,7 @@ class BlobTransaction(Container): chain_id: uint256 nonce: uint64 priority_fee_per_gas: uint256 - max_basefee_per_gas: uint256 + max_fee_per_gas: uint256 gas: uint64 to: Union[None, Address] # Address = Bytes20 value: uint256 @@ -191,7 +135,7 @@ class ECDSASignature(Container): s: uint256 ``` -The `priority_fee_per_gas` and `max_basefee_per_gas` fields follow [EIP-1559](./eip-1559.md) semantics, +The `priority_fee_per_gas` and `max_fee_per_gas` fields follow [EIP-1559](./eip-1559.md) semantics, and `access_list` as in [`EIP-2930`](./eip-2930.md). [`EIP-2718`](./eip-2718.md) is extended with a "wrapper data", the typed transaction can be encoded in two forms, dependent on the context: @@ -215,7 +159,7 @@ The execution layer verifies the wrapper validity against the inner `Transaction * There may be at most `MAX_BLOBS_PER_BLOCK` total blob commitments in a valid block. * There is an equal amount of versioned hashes, kzg commitments and blobs. * The KZG commitments hash to the versioned hashes, i.e. `kzg_to_versioned_hash(kzg[i]) == versioned_hash[i]` -* The KZG commitments match the blob contents. (Note: this can be optimized with additional data, using a proof for a +* The KZG commitments match the blob contents. (Note: this can be optimized with additional data, using a proof for a random evaluation at two points derived from the commitment and blob data) @@ -284,7 +228,7 @@ Note that the consensus-layer is tasked with persisting the blobs for data avail The `ethereum/consensus-specs` repository defines the following beacon-node changes involved in this EIP: - Beacon chain: process updated beacon blocks and ensure blobs are available. -- P2P network: gossip and sync updated beacon block types and new blobs sidecars. +- P2P network: gossip and sync updated beacon block types and new blobs sidecars. - Honest validator: produce beacon blocks with blobs, publish the blobs sidecars. ### Opcode to get versioned hashes @@ -369,53 +313,23 @@ class BlobTransactionNetworkWrapper(Container): We do network-level validation of `BlobTransactionNetworkWrapper` objects as follows: ```python -def hash_to_bls_field(x: Container) -> BLSFieldElement: - """ - This function is used to generate Fiat-Shamir challenges. The output is not uniform over the BLS field. - """ - return int.from_bytes(hash_tree_root(x), "little") % BLS_MODULUS - - -def compute_powers(x: BLSFieldElement, n: uint64) -> List[BLSFieldElement]: - current_power = 1 - powers = [] - for _ in range(n): - powers.append(BLSFieldElement(current_power)) - current_power = current_power * int(x) % BLS_MODULUS - return powers - -def vector_lincomb(vectors: List[List[BLSFieldElement]], scalars: List[BLSFieldElement]) -> List[BLSFieldElement]: - """ - Given a list of vectors, compute the linear combination of each column with `scalars`, and return the resulting - vector. - """ - r = [0]*len(vectors[0]) - for v, a in zip(vectors, scalars): - for i, x in enumerate(v): - r[i] = (r[i] + a * x) % BLS_MODULUS - return [BLSFieldElement(x) for x in r] - def validate_blob_transaction_wrapper(wrapper: BlobTransactionNetworkWrapper): versioned_hashes = wrapper.tx.message.blob_versioned_hashes commitments = wrapper.blob_kzgs blobs = wrapper.blobs # note: assert blobs are not malformatted - assert len(versioned_hashes) == len(commitments) == len(blobs) - number_of_blobs = len(blobs) - # Generate random linear combination challenges - r = hash_to_bls_field([blobs, commitments]) - r_powers = compute_powers(r, number_of_blobs) - - # Compute commitment to aggregated polynomial - aggregated_poly_commitment = lincomb(commitments, r_powers) - - # Create aggregated polynomial in evaluation form - aggregated_poly = vector_lincomb(blobs, r_powers) + aggregated_poly, aggregated_poly_commitment = compute_aggregated_poly_and_commitment( + blobs, + commitments, + ) # Generate challenge `x` and evaluate the aggregated polynomial at `x` - x = hash_to_bls_field([aggregated_poly, aggregated_poly_commitment]) + x = hash_to_bls_field( + PolynomialAndCommitment(polynomial=aggregated_poly, kzg_commitment=aggregated_poly_commitment) + ) + # Evaluate aggregated polynomial at `x` (evaluation function checks for div-by-zero) y = evaluate_polynomial_in_evaluation_form(aggregated_poly, x) # Verify aggregated proof diff --git a/EIPS/eip-4895.md b/EIPS/eip-4895.md index d0f199df2de987..bcdb5c1e8e774a 100644 --- a/EIPS/eip-4895.md +++ b/EIPS/eip-4895.md @@ -4,7 +4,7 @@ title: Beacon chain push withdrawals as operations description: Support validator withdrawals from the beacon chain to the EVM via a new "system-level" operation type. author: Alex Stokes (@ralexstokes), Danny Ryan (@djrtwo) discussions-to: https://ethereum-magicians.org/t/eip-4895-beacon-chain-withdrawals-as-system-level-operations/8568 -status: Draft +status: Review type: Standards Track category: Core created: 2022-03-10 @@ -22,10 +22,10 @@ This EIP provides a way for validator withdrawals made on the beacon chain to en The architecture is "push"-based, rather than "pull"-based, where withdrawals are required to be processed in the execution layer as soon as they are dequeued from the consensus layer. Withdrawals are represented as a new type of object in the execution payload -- an "operation" -- that separates the withdrawals feature from user-level transactions. -This approach is more involved than the prior [EIP-4863](./eip-4863.md) but it cleanly separates this "system-level" operation from regular transactions. +This approach is more involved than the prior approach introducing a new transaction type but it cleanly separates this "system-level" operation from regular transactions. The separation simplifies testing (so facilitates security) by reducing interaction effects generated by mixing this system-level concern with user data. -Moreover, this approach is more complex than "pull"-based alternatives (e.g. [EIP-4788](./eip-4788.md) + user-space withdrawal contract) with respect to the core protocol but does provide tighter integration of a critical feature into the protocol itself. +Moreover, this approach is more complex than "pull"-based alternatives with respect to the core protocol but does provide tighter integration of a critical feature into the protocol itself. ## Specification @@ -40,12 +40,15 @@ Beginning with the execution timestamp `FORK_TIMESTAMP`, execution clients **MUS Define a new payload-level object called a `withdrawal` that describes withdrawals that have been validated at the consensus layer. `Withdrawal`s are syntactically similar to a user-level transaction but live in a different domain than user-level transactions. -`Withdrawal`s have three key pieces of information supplied from the consensus layer: -1. a monotonically increasing `index` as a `uint64` value that uniquely identifies each withdrawal -2. a recipient for the withdrawn ether `address` as a 20-byte value -3. an `amount` of ether given in wei as a 256-bit value. +`Withdrawal`s provide key information from the consensus layer: +1. a monotonically increasing `index`, starting from 0, as a `uint64` value that increments by 1 per withdrawal to uniquely identify each withdrawal +2. the `validator_index` of the validator on the consensus layer the withdrawal corresponds to +3. a recipient for the withdrawn ether `address` as a 20-byte value +4. an `amount` of ether given in wei as a 256-bit value. -`Withdrawal` objects are serialized as a RLP list according to the schema: `[index, address, amount]`. +*NOTE*: the `index` for each withdrawal is a global counter spanning the entire sequence of withdrawals. + +`Withdrawal` objects are serialized as a RLP list according to the schema: `[index, validator_index, address, amount]`. ### New field in the execution payload: withdrawals @@ -54,8 +57,8 @@ The execution payload gains a new field for the `withdrawals` which is an RLP li For example: ```python -withdrawal_0 = [index_0, address_0, amount_0] -withdrawal_1 = [index_1, address_1, amount_1] +withdrawal_0 = [index_0, validator_index_0, address_0, amount_0] +withdrawal_1 = [index_1, validator_index_1, address_1, amount_1] withdrawals = [withdrawal_0, withdrawal_1] ``` @@ -145,7 +148,7 @@ An entirely new type of object firewalls off generic EVM execution from this typ The maximum number of withdrawals that can reach the execution layer at a given time is bounded (enforced by the consensus layer) and this limit has been chosen so that any execution layer operational costs are negligible in the context of the broader payload execution. -This bound applies to both computational cost (only a few balance updates in the state) and storage/networking cost as the additional payload footprint is kept small (current parameterizations put the additional overhead at ~1% of current average payload size). +This bound applies to both compuggational cost (only a few balance updates in the state) and storage/networking cost as the additional payload footprint is kept small (current parameterizations put the additional overhead at ~1% of current average payload size). ### Why only balance updates? No general EVM execution? diff --git a/EIPS/eip-4955.md b/EIPS/eip-4955.md index 5f36bc54992b1c..87e045447fa20c 100644 --- a/EIPS/eip-4955.md +++ b/EIPS/eip-4955.md @@ -1,10 +1,10 @@ --- eip: 4955 -title: Vendor Specific Metadata Extension for Non-Fungible Tokens +title: Vendor Metadata Extension for NFTs description: Add a new field to NFT metadata to store vendor specific data author: Ignacio Mazzara (@nachomazzara) discussions-to: https://ethereum-magicians.org/t/eip-4955-non-fungible-token-metadata-namespaces-extension/8746 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-03-29 @@ -13,23 +13,21 @@ requires: 721, 1155 ## Abstract -The following standard allows for the implementation of a standard schema for NFTs metadata. The main goal is adding a new field namespaces to the JSON schema for NFTs +This EIP standardizes a schema for NFTs metadata to add new field namespaces to the JSON schema for [EIP-721](./eip-721.md) and [EIP-1155](./eip-1155.md) NFTs. ## Motivation -A standard schema allows wallets, marketplaces, metaverses, etc. to work with any NFT. +A standardized NFT metadata schema allows wallets, marketplaces, metaverses, and sililar applications to interoperate with any NFT. Applications such as NFT marketplaces and metaverses could usefully leverage NFTs by rendering them using custom 3D representations or any other new attributes. -This standard is inspired by the projects like marketplaces and metaverses which want to leverage the usage of NFTs by rendering them using custom 3d representations or any other new attributes. - -Some projects like Decentraland, TheSandbox, Cryptoavatars, etc. need their own 3d model in order to represent an NFT. These models are not compatible between them because of their esthetics and armatures. - -The lack of a property like the one proposed makes it almost impossible to generate interoperability of NFTs and keep the data decentralized. +Some projects like Decentraland, TheSandbox, Cryptoavatars, etc. need their own 3D model in order to represent an NFT. These models are not cross-compatible because of distinct aesthetics and data formats. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. -**Proposed Schema** (subject to "caveats" below): +### Schema + +(subject to "caveats" below) A new property called `namespaces` is introduced. This property expects one object per project as shown in the example below. @@ -58,7 +56,8 @@ A new property called `namespaces` is introduced. This property expects one obje } ``` -**Example** +### Example + ```json { "name": "My NFT", @@ -95,18 +94,20 @@ There are many projects which need custom properties in order to display a curre The main differences between the projects that are rendering 3d NFTs (models) are: -- **Armatures** -For example, every metaverse uses its own armature. There is a standard for humanoids but it is not being used for every metaverse and not all the metaverses use humanoids. For example, Decentraland has a different esthetic than Cryptovoxels and TheSandbox. It means that every metaverse will need a different model and they may have the same extension (GLB, GLTF) +### Armatures + +Every metaverse uses its own armature. There is a standard for humanoids but it is not being used for every metaverse and not all the metaverses use humanoids. For example, Decentraland has a different aesthetic than Cryptovoxels and TheSandbox. It means that every metaverse will need a different model and they may have the same extension (GLB, GLTF) EIP 4955 Different Renders -- **Metadata (Representations Files)** +### Metadata (Representations Files) For example, every metaverse uses its own metadata representation files to make it work inside the engine depending on its game needs. This is how a wearable looks like in Decentraland in terms of the config file: -``` + +```json "data": { "replaces": [], "hides": [], @@ -154,13 +155,14 @@ This is how a wearable looks like in Decentraland in terms of the config file: "entities": 1 } ``` + `replaces`, `overrides`, `hides`, and different body shapes representation for the same asset are needed for Decentraland in order to render the 3D asset correctly. --- -Using `namespaces` instead of objects like the ones below make it easy for the specific vendor/third-parties to access and index the required models. Moreover, `styles` do not exist because there are no standards around for how an asset will be rendered. As I mentioned above, each metaverse for example uses its own armature and esthetic. There is no Decentraland-style or TheSandbox-style that other metaverses use. Each of them is unique and specific for the sake of the platform's reason of being. Projects like Cryptoavatars are trying to push different standards but without luck for the same reasons related to the uniquity of the armature/animations/metadata. +Using `namespaces` instead of objects like the ones below make it easy for the specific vendor/third-parties to access and index the required models. Moreover, `styles` do not exist because there are no standards around for how an asset will be rendered. As I mentioned above, each metaverse for example uses its own armature and aesthetic. There is no Decentraland-style or TheSandbox-style that other metaverses use. Each of them is unique and specific for the sake of the platform's reason of being. Projects like Cryptoavatars are trying to push different standards but without luck for the same reasons related to the uniquity of the armature/animations/metadata. -``` +```json { "id": "model", "type": "model/gltf+json", @@ -184,12 +186,16 @@ With `namespaces` each vendor will know how to render an asset by doing: fetch(metadata.namespaces["PROJECT_NAME"].uri).then(res => render(res)) ``` -The idea behind extending the ERC721 metadata schema is to have backward compatibility as much as possible with existing projects. Chances are that the asset's smart contracts are not upgradeable and therefore if a project wants to be compatible with this EIP, it will need to redeploy and migrate the current contracts. This is very time- and money-consuming. Creating a new token standard that stores the data needed on-chain: 3D models and config files are not the right paths. There are protocols already used for the ERC721 metadata standard like IPFS (the token URI is an IPFS hash). The idea is to leverage this and require as few changes as possible. Moreover, the current metadata standard uses a 2D representation field: `image`. It seems reasonable to have all the representations of an asset in the same place. +The idea behind extending the [EIP-721](./eip-721.md) metadata schema is to have backward compatibility as much as possible with existing projects. Chances are that the asset's smart contracts are not upgradeable and therefore if a project wants to be compatible with this EIP, it will need to redeploy and migrate the current contracts. This is very time- and money-consuming. Creating a new token standard that stores the data needed on-chain: 3D models and config files are not the right paths. There are protocols already used for the [EIP-721](./eip-721.md) metadata standard like IPFS (the token URI is an IPFS hash). The idea is to leverage this and require as few changes as possible. Moreover, the current metadata standard uses a 2D representation field: `image`. It seems reasonable to have all the representations of an asset in the same place. ## Backwards Compatibility Existing projects that can't modify the metadata response (schema), may be able to create a new smart contract that based on the `tokenId` returns the updated metadata schema. Of course, the projects may need to accept these linked smart contracts as valid in order to fetch the metadata by the `tokenURI` function. +## Security Considerations + +The same security considerations as with [EIP-721](./eip-721.md) apply related to using http gateways or IPFS for the tokenURI method. + ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-4973.md b/EIPS/eip-4973.md index a03e78cd84c5be..0a0d042b9ee14c 100644 --- a/EIPS/eip-4973.md +++ b/EIPS/eip-4973.md @@ -54,7 +54,7 @@ pragma solidity ^0.8.6; /// @title Account-bound tokens /// @dev See https://eips.ethereum.org/EIPS/eip-4973 -/// Note: the ERC-165 identifier for this interface is 0x5164cf47 +/// Note: the ERC-165 identifier for this interface is 0x8d7bac72 interface IERC4973 { /// @dev This emits when ownership of any ABT changes by any mechanism. /// This event emits when ABTs are given or equipped and unequipped diff --git a/EIPS/eip-5006.md b/EIPS/eip-5006.md index dd2369c50b0508..996afb18a72154 100755 --- a/EIPS/eip-5006.md +++ b/EIPS/eip-5006.md @@ -45,10 +45,10 @@ interface IERC5006 { event CreateUserRecord( uint256 recordId, uint256 tokenId, - uint256 amount, + uint64 amount, address owner, address user, - uint64 expiry + uint64 expiry ); /** diff --git a/EIPS/eip-5023.md b/EIPS/eip-5023.md new file mode 100644 index 00000000000000..ae4c0fc0372109 --- /dev/null +++ b/EIPS/eip-5023.md @@ -0,0 +1,168 @@ +--- +eip: 5023 +title: Shareable Non-Fungible Token +description: An interface for creating value-holding tokens shareable by multiple owners +author: Jarno Marttila (@yaruno), Martin Moravek (@mmartinmo) +discussions-to: https://ethereum-magicians.org/t/new-nft-concept-shareable-nfts/8681 +status: Draft +type: Standards Track +category: ERC +created: 2022-01-28 +requires: 165 +--- + +## Abstract + +This EIP standardizes an interface for non-fungible value-holding shareable tokens. Shareability is accomplished by minting copies of existing tokens for new recipients. Sharing and associated events allow the construction of a graph describing who has shared what to which party. + + +## Motivation + +NFT standards such as [EIP-721](./eip-721.md) and [EIP-1155](./eip-1155.md) have been developed to standardize scarce digital resources. However, many non-fungible digital resources need not be scarce. + +We have attempted to capture positive externalities in ecosystems with new types of incentive mechanisms that exhibit anti-rival logic, serve as an unit of accounting and function as medium of sharing. We envision that shareable tokens can work both as incentives but also as representations of items that are typically digital in their nature and gain more value as they are shared. + +These requirements have set us to define shareable NFTs and more specifically a variation of shareable NFTs called non-transferable shareable NFTs. These shareable NFTs can be “shared” in the same way digital goods can be shared, at an almost zero technical transaction cost. We have utilized them to capture anti-rival value in terms of accounting positive externalities in an economic system. + +Typical NFT standards such as EIP-721 and EIP-1155 do not define a sharing modality. Instead ERC standards define interfaces for typical rival use cases such as token minting and token transactions that the NFT contract implementations should fulfil. The ‘standard contract implementations' may extend the functionalities of these standards beyond the definition of interfaces. The shareable tokens that we have designed and developed in our experiments are designed to be token standard compatible at the interface level. However the implementation of token contracts may contain extended functionalities to match the requirements of the experiments such as the requirement of 'shareability'. In reflection to standard token definitions, shareability of a token could be thought of as re-mintability of an existing token to another party while retaining the original version of it. + +Sharing is an interesting concept as it can be thought and perceived in different ways. For example, when we talk about sharing we can think about it is as digital copying, giving a copy of a digital resource while retaining a version by ourselves. Sharing can also be fractional or sharing could be about giving rights to use a certain resource. The concept of shareability and the context of shareability can take different forms and one might use different types of implementatins for instances of shareable tokens. Hence we haven't restricted that the interface should require any specific token type. + +Shareable tokens can be made non-transferable at the contract implementaiton level. Doing so, makes them shareable non-transferable tokens. In the reference implementation we have distilled a general case from our use cases that defines a shareable non-transferable NFTs using the shareable NFT interface. + +We believe that the wider audience should benefit from an abstraction level higher definition for shareability, such as this interface implementation, that defines minimum amount of functions that would be implemented to satisfy the concept of shareability. + +## Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +```solidity +/// Note: the ERC-165 identifier for this interface is 0xd763f0cf +interface IERC5023 is IERC165 { + + /// @dev This emits when a token is shared, reminted and given to another wallet that isn't function caller + event Share(address indexed from, address indexed to, uint256 indexed tokenId, uint256 derivedFromtokenId); + + /// @dev Shares, remints an existing token, gives a newly minted token a fresh token id, keeps original token at function callers possession and transfers newly minted token to receiver which should be another address than function caller. + function share(address to, uint256 tokenIdToBeShared) returns(uint256 newTokenId) external; + +} +``` + +Share event is expected to be emitted when function method share is succesfully called and a new token on basis of a given token id is minted and transferred to a recipient. + +## Rationale + +Current NFT standards define transferable non-fungible tokens, but not shareable non-fungible tokens. To be able to create shareable NFTs we see that existing NFT contracts could be extended with an interface which defines the basic principles of sharing, namely the Event of sharing and the function method of sharing. Definition of how transferability of tokens should be handled is left to the contract implementor. In case transfering is left enable shareable tokens behave similarily to the existing tokens, except when they are shared, a version of token is retained. In case transfering is disabled, shareable tokens become shareable non-transferable tokens, where they can be minted and given or shared to other people, but they cannot be transferred away. + +Imagine that Bob works together with Alice on a project. Bob earns an unique NFT indicating that he has made effort to the project, but Bob feels that his accomplishments are not only out of his own accord. Bob wants to share his token with Alice to indicate that also Alice deserves recognition of having put effort on their project. Bob initiates token sharing by calling `Share` method on the contract which has his token and indicates which one of his tokens he wishes to share and to whom by passing address and token id parameters. A new token is minted for Alice and a `Share` event is initiated to communicate that it was Bob whom shared his token to Alice by logging addresses who shared a token id to whose address and which token id was this new token derived from. + +Over time, a tree-like structures can be formed from the Share event information. If Bob shared to Alice, and Alice shared further to Charlie and Alice also shared to David a rudimentary tree structure forms out from sharing activity. This share event data can be later on utilized to gain more information of share activities that the tokens represent. + +``` +B -> A -> C + \ + > D +``` + +These tree structures can be further aggregated and collapsed to network representations e.g. social graphs on basis of whom has shared to whom over a span of time. E.g. if Bob shared a token to Alice, and Alice has shared a different token to Charlie and Bob has shared a token to Charlie, connections form between all these parties through sharing activities. +``` + B----A----C + \_______/ +``` + +## Backwards Compatibility + +TBD + +## Reference Implementation + +Following reference implementation demonstrates a general use case of one of our pilots. In this case a shareable non-transferable token represents a contribution done to a community that the contract owner has decided to merit with a token. Contract owner can mint a merit token and give it to a person. This token can be further shared by the receiver to other parties for example to share the received merit to others that have participated or influenced his contribution. + +``` +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5023.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract ShareableERC721 is ERC721URIStorage, Ownable, IERC5023 /* EIP165 */ { + + string baseURI; + + uint256 internal _currentIndex; + + constructor(string memory _name, string memory _symbol) ERC721(_name, _symbol) {} + + function mint( + address account, + uint256 tokenId + ) external onlyOwner { + _mint(account, tokenId); + } + + function setTokenURI( + uint256 tokenId, + string memory tokenURI + ) external { + _setTokenURI(tokenId, tokenURI); + } + + function setBaseURI(string memory baseURI_) external { + baseURI = baseURI_; + } + + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + function share(address to, uint256 tokenIdToBeShared) external returns(uint256 newTokenId) { + require(to != address(0), "ERC721: mint to the zero address"); + require(_exists(tokenIdToBeShared), "ShareableERC721: token to be shared must exist"); + + require(msg.sender == ownerOf(tokenIdToBeShared), "Method caller must be the owner of token"); + + string memory _tokenURI = tokenURI(tokenIdToBeShared); + _mint(to, _currentIndex); + _setTokenURI(_currentIndex, _tokenURI); + + emit Share(msg.sender, to, _currentIndex, tokenIdToBeShared); + + return _currentIndex; + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + revert('In this reference implementation tokens are not transferrable'); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + revert('In this reference implementation tokens are not transferrable'); + } +} + + + +``` +## Security Considerations + +TBD + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5027.md b/EIPS/eip-5027.md index ec3984a1d75259..8d0e9207387e2f 100644 --- a/EIPS/eip-5027.md +++ b/EIPS/eip-5027.md @@ -8,7 +8,7 @@ status: Draft type: Standards Track category: Core created: 2022-04-21 -requires: 170 +requires: 170, 2929, 2930 --- @@ -36,24 +36,28 @@ The proposal implements a solution to remove the existing 24576-bytes limit of t | ------------------------- | ---------------- | | `FORK_BLKNUM` | TBD | | `CODE_SIZE_UNIT` | 24576 | -| `READ_GAS_PER_UNIT` | 700 | +| `COLD_ACCOUNT_CODE_ACCESS_COST_PER_UNIT` | 2600 | | `CREATE_DATA_GAS` | 200 | If `block.number >= FORK_BLKNUM`, the contract creation initialization can return data with any length, but the contract-related opcodes will take extra gas as defined below: - For `CODESIZE/CODECOPY/EXTCODESIZE/EXTCODEHASH`, the gas is unchanged. +- For CREATE/CREATE2, if the newly created contract size > `CODE_SIZE_UNIT`, the opcodes will take extra write gas as + +`(CODE_SIZE - CODE_SIZE_UNIT) * CREATE_DATA_GAS`. + - For `EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL`, if the contract code size > `CODE_SIZE_UNIT`, then the opcodes will take extra gas as ``` -(CODE_SIZE - 1) // CODE_SIZE_UNIT * READ_GAS_PER_UNIT +(CODE_SIZE - 1) // CODE_SIZE_UNIT * COLD_ACCOUNT_CODE_ACCESS_COST_PER_UNIT ``` -where `//` is the integer divide operator. +if the contract is not in `accessed_code_in_addresses` or `0` if the contract is in `accessed_code_in_addresses`, where `//` is the integer divide operator, and `accessed_code_in_addresses: Set[Address]` is a tranasction-context-wide set similar to `access_addressses` and `accessed_storage_keys`. -- For CREATE/CREATE2, if the newly created contract size > `CODE_SIZE_UNIT`, the opcodes will take extra write gas as +When a transactoin execution begins, `accessed_code_in_addresses` will include `tx.sender`, `tx.to`, and all precompiles. -`(CODE_SIZE - CODE_SIZE_UNIT) * CREATE_DATA_GAS`. +When `CREATE/CREATE2/EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL` is called, immediately add the address to `accessed_code_in_addresses`. ## Rationale @@ -66,16 +70,16 @@ The goal is to measure the CPU/IO cost of the contract read/write operations reu - For `EXTCODEHASH`, the value is already in the account, so we do not charge extra gas. -- For `EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL`, since it will read extra data from the database, we will additionally charge READ_GAS_PER_UNIT per extra `CODE_SIZE`. Note that `READ_GAS_PER_UNIT = CALLGAS = EXTCODECOPYBASE = 700`. +- For `EXTCODECOPY/CALL/CALLCODE/DELEGATECALL/STATICCALL`, since it will read extra data from the database, we will additionally charge `COLD_ACCOUNT_CODE_ACCESS_COST_PER_UNIT` per extra `CODE_SIZE_UNIT`. - For `CREATE/CREATE2`, since it will create extra data to the database, we will additionally charge `CREATE_DATA_GAS` per extra bytes. -## Backward Compatibility +## Backwards Compatibility All existing contracts will not be impacted by the proposal. -Only contracts deployed before EIP-170 could possibly be longer than the current max code size, and the reference implementation was able to successfully import all blocks before that fork. +Only contracts deployed before [EIP-170](./eip-170.md) could possibly be longer than the current max code size, and the reference implementation was able to successfully import all blocks before that fork. ## Reference Implementation diff --git a/EIPS/eip-5173.md b/EIPS/eip-5173.md index a14cbbc21c6a29..7a1336b9add7f3 100644 --- a/EIPS/eip-5173.md +++ b/EIPS/eip-5173.md @@ -141,7 +141,7 @@ A default `FRInfo` MUST be stored in order to be backward compatible with [EIP-7 ### EIP-721 Overrides -An nFR-compliant smart contract MUST override the [EIP-721](./eip-721.md) `_mint`, `_transfer`, and `_burn` functions. When overriding the `_mint` function, a default FR model is REQUIRED to be established if the mint is to succeed when calling the [EIP-721](./eip-721.md) `_mint` function and not the nFR `_mint` function. It is also to update the owner amount and directly add the recipient address to the FR cycle. When overriding the `_transfer` function, the smart contract SHALL consider the NFT as sold for 0 ETH, and update the state accordingly after a successful transfer. This is to prevent FR circumvention. Finally, when overriding the `_burn` function, the smart contract SHALL delete the `FRInfo` corresponding to that Token ID after a successful burn. +An nFR-compliant smart contract MUST override the [EIP-721](./eip-721.md) `_mint`, `_transfer`, and `_burn` functions. When overriding the `_mint` function, a default FR model is REQUIRED to be established if the mint is to succeed when calling the [EIP-721](./eip-721.md) `_mint` function and not the nFR `_mint` function. It is also to update the owner amount and directly add the recipient address to the FR cycle. When overriding the `_transfer` function, the smart contract SHALL consider the NFT as sold for 0 ETH, and update the state accordingly after a successful transfer. This is to prevent FR circumvention. Additionally, the `_transfer` function SHALL prevent the caller from transferring the token to themselves, this can be done through a require statement that ensures the sender is not the recipient, otherwise, it’d be possible to fill up the FR sequence with one’s own address. Finally, when overriding the `_burn` function, the smart contract SHALL delete the `FRInfo` corresponding to that Token ID after a successful burn. Additionally, the [EIP-721](./eip-721.md) `_checkOnERC721Received` function MAY be explicitly called after mints and transfers if the smart contract aims to have safe transfers and mints. @@ -188,7 +188,7 @@ function transferFrom(address from, address to, uint256 tokenId, uint256 soldPri ``` -Based on the stored `lastSoldPrice`, the smart contract will determine whether the sale was profitable after calling the [EIP-721](./eip-721.md) transfer function and transferring the NFT. If it was not profitable, the smart contract SHALL update the last sold price for the corresponding Token ID, increment the owner amount, shift the generations, and transfer all of the `msg.value` to the `lister` depending on the implementation. Otherwise, if the transaction was profitable, the smart contract SHALL call the `_distributeFR` function, then update the `lastSoldPrice`, increment the owner amount, and finally shift generations. The `_distributeFR` function MUST return the difference between the allocated FR that is to be distributed amongst the `_addressesInFR` and the `msg.value` to the `lister`. Once the operations have completed, the function MUST clear the corresponding `ListInfo`. +Based on the stored `lastSoldPrice`, the smart contract will determine whether the sale was profitable after calling the [EIP-721](./eip-721.md) transfer function and transferring the NFT. If it was not profitable, the smart contract SHALL update the last sold price for the corresponding Token ID, increment the owner amount, shift the generations, and transfer all of the `msg.value` to the `lister` depending on the implementation. Otherwise, if the transaction was profitable, the smart contract SHALL call the `_distributeFR` function, then update the `lastSoldPrice`, increment the owner amount, and finally shift generations. The `_distributeFR` function MUST return the difference between the allocated FR that is to be distributed amongst the `_addressesInFR` and the `msg.value` to the `lister`. Once the operations have completed, the function MUST clear the corresponding `ListInfo`. Similarly to the `_transfer` override, the FR `_transferFrom` SHALL ensure that the recipient is not the sender of the token. ### Future Rewards Calculation diff --git a/EIPS/eip-5187.md b/EIPS/eip-5187.md index 1cc1240580cb5e..bb1ff12a15deeb 100644 --- a/EIPS/eip-5187.md +++ b/EIPS/eip-5187.md @@ -12,68 +12,85 @@ requires: 165, 1155 --- ## Abstract -This standard is an extension of [EIP-1155](./eip-1155.md). It proposes introducing the concept of independent, multiple, and leasable rights of use to enable NFT to be leased out for different cycles while ownership remains with the owner. + +This standard is an extension of [EIP-1155](./eip-1155.md). It proposes to introduce separable, rentable, and transferable usage rights (in the form of NFT-IDs), enabling the property owner (the only NFT holder) to rent out the NFT to multiple users (ID holders) at the same time for different terms, and be withdrawn by smart contract upon expiration. + +The property owner always retains ownership and is able to transfer the NFT to others during the lease. + +The proposal also supports the sublease and renewal of the rental so that users can freely transfer the usage rights among each other and extend the lease term. Early return of NFTs can also be achieved by subletting the usage rights back to the property owners. ## Motivation -The traditional [EIP-721](./eip-721.md) and EIP-1155 focus more on ownership. However, NFTs as digital assets are more prominent in use than ownership. Taking artistic NFTs as an example, NFT artists may wish to rent out the use rights of their works to media companies for an allotted time, or NFT musicians may wish to make their music available to listeners as per playing duration. -Therefore, to better serve NFT developers to meet such needs and develop more sophisticated NFT products, we propose directly introducing rentable usage rights to complement the ERC standard. + +The well-accepted [EIP-721](./eip-721.md) and EIP-1155 standards focused on the ownership of unique assets, quite sensible in the time of NFTs being used primarily as arts and collectibles, or, you can say, as private property rights. +### First Step: "Expirable" NFTs +The advent of private ownership in the real world has promoted the vigorous development of the modern economy, and we believe that the usage right will be the first detachable right widely applied in the blockchain ecosystem. As NFTs are increasingly applied in rights, finance, games, and the Metaverse, the value of NFT is no longer simply the proof of ownership, but with limitless practice use scenarios. For example, artists may wish to rent out their artworks to media or audiences within specific periods, and game guilds may wish to rent out game items to new players to reduce their entry costs. + +The lease/rental of NFTs in the crypto space is not a new topic, but the implementation of leasing has long relied on over-collateralization, centralized custody, or pure trust, which significantly limits the boom of the leasing market. Therefore, a new type of "expirable" NFTs that can be automatically withdrawn upon expiration through smart contract is proposed, at the technical level, to eliminate those bottlenecks. Based on that, a new leasing model that is decentralized, collateral-free, and operated purely "on-chain" may disrupt the way people trade and use NFTs. Thus, this EIP proposal is here to create "expirable" NFTs compatible with EIP-1155. +### Then, Make Everything Transferable +The way we achieve leasing is to separate ownership and usage rights, and beyond that, we focus more on making them freely priced and traded after separation, which is impossible to happen in the traditional financial field. Imagine the below scenarios: i) as a landlord, you can sell your house in rental to others without affecting the tenancy, and your tenants will then pay rent to the new landlord; ii) as a tenant, you can sublet the house to others without the consent of the landlord, and even the one sublets can continue subletting the house until the lease term is close the last tenant can apply for a renewal of the lease. All of this can happen in the blockchain world, and that's the beauty of blockchain. Without permission, without trust, code is the law. + +Making ownership and usage rights transferable may further revolutionize the game rules in NFT's field, both in capital allocation and NFT development. Buying NFT ownership is more like investing in stocks, and the price is determined by market expectations of the project; renting the usage right is less speculative, so the price is easier to determine based on supply and demand. The ownership market and the usage-right market will function to meet the needs of target participants and achieve a balance that is conducive to the long-term and stable development of NFT projects. +Based on the above, we propose this EIP standard to complement the current EIP scopes and introduce those functions as new standards. ## Specification + The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. ```solidity pragma solidity ^0.8.0; -/// Note: the ERC-165 identifier for this interface is 0xd4613e9f. -interface IRental /* is IERC1155,IERC165 */ { +/// Note: the ERC-165 identifier for this interface is 0x6938e358. + interface IRental /* is IERC165,IERC1155 */ { /** * @notice This emits when user rent NFT - * @param id The id of the current token - * @param user The address to rent the NFT usage rights - * @param amount The amount of usage rights - * @param expires The specified period of time to rent + * - `id` The id of the current token + * - `user` The address to rent the NFT usage rights + * - `amount` The amount of usage rights + * - `expire` The specified period of time to rent **/ - event Rented( - uint256 indexed id, - address indexed user, - uint256 amount, - uint256 expires - ); + event Rented(uint256 indexed id,address indexed user,uint256 amount,uint256 expire); /** - * @notice This emits when the NFT owner takes back the usage rights from the tenant (the `user`) - * @param id The id of the current token - * @param user The address to rent the NFT's usage rights - * @param amount Amount of usage rights + * MUST trigger on any successful call to `renew(address user,uint256 id)` + * - `id` The id of the current token + * - `user` The user of the NFT + * - `expire` The new specified period of time to rent + **/ + event Renew(uint256 indexed id,address indexed user,uint256 expire); + + /** + * MUST trigger on any successful call to `renew(address user,uint256 id,uint256 expire)` + * - `id` The id of the current token + * - `from` The current user of the NFT + * - `to` The new user + **/ + event Sublet(uint256 indexed id,address indexed from,address to); + + /** + * @notice This emits when the NFT owner takes back the usage rights from the tenant (the `user`) + * - id The id of the current token + * - user The address to rent the NFT's usage rights + * - amount Amount of usage rights **/ event TakeBack(uint256 indexed id, address indexed user, uint256 amount); /** * @notice Function to rent out usage rights - * @param from The address to approve - * @param to The address to rent the NFT usage rights - * @param id The id of the current token - * @param amount The amount of usage rights - * @param expires The specified period of time to rent + * - from The address to approve + * - to The address to rent the NFT usage rights + * - id The id of the current token + * - amount The amount of usage rights + * - expire The specified period of time to rent **/ - function safeRent( - address from, - address to, - uint256 id, - uint256 amount, - uint256 expires - ) external; + function safeRent(address from,address to,uint256 id,uint256 amount,uint256 expire) external; /** * @notice Function to take back usage rights after the end of the tenancy - * @param user The address to rent the NFT's usage rights - * @param tokenId The id of the current token + * - user The address to rent the NFT's usage rights + * - tokenId The id of the current token **/ - function takeBack( - address user, - uint256 tokenId - ) external; + function takeBack(address user,uint256 tokenId) external; /** * @notice Return the NFT to the address of the NFT property right owner. @@ -84,18 +101,48 @@ interface IRental /* is IERC1155,IERC165 */ { * @notice Return the total supply amount of the current token **/ function totalSupply(uint256 id) external view returns (uint256); + + /** + * @notice Return expire The specified period of time to rent + **/ + function expireAt(uint256 id,address user) external view returns(uint256); + + /** + * extended rental period + * - `id` The id of the current token + * - `user` The user of the NFT + * - `expire` The new specified period of time to rent + **/ + function renew(address user,uint256 id,uint256 expire) external; + + /** + * transfer of usage right + * - `id` The id of the current token + * - `user` The user of the NFT + * - `expire` The new specified period of time to rent + **/ + function sublet(address to,uint256 id) external; } ``` + ## Rationale -There are two main benefits to creating this proposal. One is to make it possible to create NFTs with detachable usage rights, of which the transfer of usage rights is separated from the transfer of ownership. The NFT owner can execute the safeRent function to lease out the usage rights to other users and the takeBack function to retrieve the usage rights after the lease expiration. + +Implementing the proposal to create rentable NFTs has two main benefits. + +One is that NFTs with multiple usage rights allow NFT property owners to perform the safeRent function and rent out usage rights to multiple users at the same time. For each usage right leased and expires, the property owner can perform the takeBack function to retrieve the usage right. + +Another benefit is that the transfer of usage rights can be quite flexible. The user can transfer the usage rights to other users by calling the Sublet function during the lease period, and can also extend the lease period of the usage rights by asking the property owner to perform the Renewal function. It is worth mentioning that if the user sublet the NFT to the property owner, it will realize the early return of NFT before the end of the lease period. ## Backwards Compatibility + As mentioned at the beginning, this is an extension of EIP-1155. Therefore, it is fully backward compatible with EIP-1155. ## Security Considerations + Needs discussion. ## Copyright + Disclaimer of copyright and related rights through [CC0](../LICENSE.md). diff --git a/EIPS/eip-5216.md b/EIPS/eip-5216.md index 9f692450bd2e29..1e16fd4ce598cb 100644 --- a/EIPS/eip-5216.md +++ b/EIPS/eip-5216.md @@ -4,7 +4,8 @@ title: EIP-1155 Approval By Amount Extension description: Extension for EIP-1155 secure approvals author: Iván Mañús (@ivanmmurciaua), Juan Carlos Cantó (@EscuelaCryptoES) discussions-to: https://ethereum-magicians.org/t/eip-erc1155-approval-by-amount/9898 -status: Review +status: Last Call +last-call-deadline: 2022-11-12 type: Standards Track category: ERC created: 2022-07-11 @@ -13,11 +14,11 @@ requires: 20, 165, 1155 ## Abstract -This specification defines standard functions for granular approval of [EIP-1155](./eip-1155.md) tokens by both `id` and `amount`. This EIP extends [EIP-1155](./eip-1155.md). +This EIP defines standard functions for granular approval of [EIP-1155](./eip-1155.md) tokens by both `id` and `amount`. This EIP extends [EIP-1155](./eip-1155.md). ## Motivation -[EIP-1155](./eip-1155.md)'s popularity means that multi-token management transactions occur on a daily basis. Although it can be used as a more comprehensive alternative to [EIP-721](./eip-721.md), it is most commonly used as intended: creating multiple `id`s, each with multiple tokens. While many projects interface with these semi-fungible tokens, by far the most common interactions are with NFT marketplaces. +[EIP-1155](./eip-1155.md)'s popularity means that multi-token management transactions occur on a daily basis. Although it can be used as a more comprehensive alternative to [EIP-721](./eip-721.md), EIP-1155 is most commonly used as intended: creating multiple `id`s, each with multiple tokens. While many projects interface with these semi-fungible tokens, by far the most common interactions are with NFT marketplaces. Due to the nature of the blockchain, programming errors or malicious operators can cause permanent loss of funds. It is therefore essential that transactions are as trustless as possible. EIP-1155 uses the `setApprovalForAll` function, which approves ALL tokens with a specific `id`. This system has obvious minimum required trust flaws. This EIP combines ideas from [EIP-20](./eip-20.md) and [EIP-721](./eip-721.md) in order to create a trust mechanism where an owner can allow a third party, such as a marketplace, to approve a limited (instead of unlimited) number of tokens of one `id`. diff --git a/EIPS/eip-5218.md b/EIPS/eip-5218.md index 266286e30bcb59..5c3222b821afd8 100644 --- a/EIPS/eip-5218.md +++ b/EIPS/eip-5218.md @@ -182,6 +182,8 @@ The `supportsInterface` method MUST return `true` when called with `0xac7b5ca9`. This EIP aims to allow tracing all licenses to an NFT to facilitate right management. The EIP-721 standard only logs the property but not the legal rights tethered to NFTs. Even when logging the license via the optional EIP-721 Metadata extension, sublicenses are not traceable, which doesn't comply with the transparency goals of Web3. Some implementations attempt to get around this limitation by minting NFTs to represent a particular license, such as the BAYC #6068 Royalty-Free Usage License. This is not an ideal solution because the linking between different licenses to an NFT is ambiguous. An auditor has to investigate all NFTs in the blockchain and inspect the metadata which hasn't been standardized in terms of sublicense relationship. To avoid these problems, this EIP logs all licenses to an NFT in a tree data structure, which is compatible with EIP-721 and allows efficient traceability. +This EIP attempts to tether NFTs with copyright licenses to the creative work by default and is not subject to the high legal threshold for copyright ownership transfers which require an explicit signature from the copyright owner. To transfer and track copyright ownership, one may possibly integrate EIP-5218 and [EIP-5289](./eip-5289.md) after careful scrutinizing and implement a smart contract that atomically (1) signs the legal contract via EIP-5289, and (2) transfers the NFT together with the copyright ownership via EIP-5218. Either both take place or both revert. + ## Backwards Compatibility This standard is compatible with the current EIP-721 standards: a contract can inherit from both EIP-721 and EIP-5218 at the same time. diff --git a/EIPS/eip-5375.md b/EIPS/eip-5375.md index a84b76bed8ca64..5ba553ce882c60 100644 --- a/EIPS/eip-5375.md +++ b/EIPS/eip-5375.md @@ -4,7 +4,8 @@ title: NFT Author Information and Consent description: An extension of EIP-721 for NFT authorship and author consent. author: Samuele Marro (@samuelemarro), Luca Donno (@lucadonnoh) discussions-to: https://ethereum-magicians.org/t/eip-5375-nft-authorship/10182 -status: Review +status: Last Call +last-call-deadline: 2022-11-06 type: Standards Track category: ERC created: 2022-07-30 diff --git a/EIPS/eip-5453.md b/EIPS/eip-5453.md index d73fb575f58997..f14e506fd7fc5d 100644 --- a/EIPS/eip-5453.md +++ b/EIPS/eip-5453.md @@ -8,7 +8,7 @@ status: Draft type: Standards Track category: ERC created: 2022-08-12 -requires: 20, 165, 721, 1155, 5269 +requires: 165 --- ## Abstract @@ -28,43 +28,47 @@ Provides a format for endorsement of smart contract transactions in the format o The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. -1. A field of method signature MUST be designated to be the endorsement field. -It must exceed a minimum length of `65 bytes` which is the length of (v, r, s) where as -- `r, s` are the ECDSA recover signature pair. `v`'s last bit carries ECDSA recover Y-Parity. -2. For any complying method of compliant contract, the last data field MUST be `bytes`. -3. Such data field MUST conform to the following format - 1. the last 8-bytes MUST be a magic 8-bytes word in the ASCII representation of "ENDORSED". Implementing method of smart contract that honors smart endorsement MUST deemed a transaction unendorsed by anyone, -if the ending 8-bytes doesn't matches with this magic word. - 2. the last 2-bytes MUST be a type-indicator, denoted as `Erc5453FormatType` - 3. For `Erc5453FormatType == FIRST_TYPE_SINGLE_ENDORSER`, it supports a simple endorsement signature in the following format. Any complying contract MUST implement FIRST_TYPE. Other type number will be used for future extension, such as a multi-but-separate-endorsement scenario. - 4. Complying contract MUST emit the following `OnEndorsed` event when a transaction is executed because of the endorsement. Complying contract MUST NOT emit such event when a transaction is executed without taking account of the endorsement. +1. For any complying method of compliant contract, the last data field MUST be `bytes`. -```solidity -event OnEndorsed(byte32 MethodABIKeccak, address[] memory endorsers); -``` +2. Such data field MUST conform to the following format + a) the last 8-bytes MUST be a magic 8-bytes word in the ASCII representation of "ENDORSED"(`0x454e444f52534544`). Implementing method of smart contract that honors smart endorsement MUST deemed a transaction unendorsed by anyone, if the ending 8-bytes doesn't matches with this magic word. + b) the last 2-bytes _before_ the `MAGIC_WORLD` MUST be a type-indicator, denoted as `Erc5453FormatType`. -4. The format of `FIRST_TYPE_SINGLE_ENDORSER` type is +3. For `Erc5453FormatType == FIRST_TYPE_SINGLE_ENDORSER`, in which FIRST_TYPE_SINGLE_ENDORSER = `0x01`, it supports a simple endorsement signature in the following format. -```solidity -endorser_nonce(uint256, 32bytes) || -valid_by(uint256, 32bytes) || -r,s,v (32bytes + 32bytes + 1bytes = 65bytes)|| -MAGIC_WORLD(8bytes) +4. Any complying contract MUST at lease implement `FIRST_TYPE_SINGLE_ENDORSER`. Other type number will be used for future extension, such as a multi-endorser scenario. + +5. The format of `FIRST_TYPE_SINGLE_ENDORSER` type is + +```text +endorser_addr || endorser_nonce || valid_by || r , s, v || Erc5453FormatType || MAGIC_WORLD ``` -The total length denoted as `LENGTH_OF_ENDORSEMENT = 137 bytes` +Whereas -5. The `(r,s,v)` are an ECDSA signature of all method fields plus the head remainder of `extraData` removing the last `LENGTH_OF_ENDORSEMENT` bytes. See the [Reference Implementation](#reference-implementation) for clarification. +- `endorser_addr` an `address` is the address of endorser, length = `32` bytes. +- `endorser_nonce`, an `uint256` is the nonce of endorser, lenght = `32` bytes. +- `valid_since`, an `uint256`, since which block number (inclusive) the endorsement is still considered valid. length = `32` bytes. +- `valid_by`, an `uint256`, until which block number (exclusive) the endorsement is still considered valid. length = `32` bytes. +- r,s,v are the signatures of ECDSA. `r, s` are the ECDSA recover signature pair. `v`'s last bit carries ECDSA recover Y-Parity. The total length = 32bytes + 32bytes + 1bytes = `65` bytes -## Rationale +The total length denoted as `LENGTH_OF_ENDORSEMENT` for `FIRST_TYPE_SINGLE_ENDORSER` = 193 bytes` + +6. Complying contract MUST emit the following `OnEndorsed` event when a transaction is executed because of the endorsement. Complying contract MUST NOT emit such event when a transaction is executed without taking account of the endorsement. -1. Choosing [EIP-5269](./eip-5269.md) instead of [EIP-165](./eip-165.md) because the data format cannot be represented by method signature as in [EIP-165](./eip-165.md). +```solidity +event OnEndorsed(byte4 indexed methodSelector, address[] memory endorsers); +``` + +## Rationale 1. Originally we considered adding the endorsement at the first part of `extraData`. We turn into adding the endorsement to the ending of data, which allows the following future extensions such as: - Allow general endorsable be applied - Allow nested endorsements + +> TODO consider adopt [EIP-5679](./eip-5679.md) ## Backwards Compatibility The design assumes a `bytes calldata extraData` to maximize the flexibility of future extensions. This assumption is compatible with [EIP-721](eip-721.md), [EIP-1155](eip-1155.md) and many other ERC-track EIPs. Those that aren't, such as [EIP-20](./eip-20.md), can also be updated to support it, such as using a wrapper contract or proxy upgrade. @@ -122,9 +126,11 @@ contract EndorsableERC721 is SomeERC721, ERC5453 { ## Security Considerations ### Replay Attacks + A replay attack is a type of attack on cryptography authentication. In a narrow sense, it usually refers to a type of attack that circumvents the cryptographically signature verification by reusing an existing signature for a message being signed again. Any implementations relying on this EIP must realize that all smart endorsements described here are cryptographic signatures that are *public* and can be obtained by anyone. They must foresee the possibility of a replay of the transactions not only at the exact deployment of the same smart contract, but also other deployments of similar smart contracts, or of a version of the same contract on another `chainId`, or any other similar attack surfaces. The `nonce`, `valid_since`, and `valid_by` fields are meant to restrict the surface of attack but might not fully eliminate the risk of all such attacks, e.g. see the [Phishing](#phishing) section. ### Phishing + It's worth pointing out a special form of replay attack by phishing. An adversary can design another smart contract in a way that the user be tricked into signing a smart endorsement for a seemingly legitimate purpose, but the data-to-designed matches the target application ## Copyright diff --git a/EIPS/eip-5484.md b/EIPS/eip-5484.md index 5d8d22fea07f69..a478951edac38d 100644 --- a/EIPS/eip-5484.md +++ b/EIPS/eip-5484.md @@ -1,10 +1,11 @@ --- eip: 5484 title: Consensual Soulbound Tokens -description: Interface for special NFTS with immutable ownership and pre-determined immutable burn authorization +description: Interface for special NFTs with immutable ownership and pre-determined immutable burn authorization author: Buzz Cai (@buzzcai) discussions-to: https://ethereum-magicians.org/t/eip-5484-consensual-soulbound-tokens/10424 -status: Review +status: Last Call +last-call-deadline: 2022-11-05 type: Standards Track category: ERC created: 2022-08-17 @@ -71,6 +72,7 @@ interface IERC5484 { ``` ## Rationale + ### Soulbound Token (SBTs) as an extension to EIP-721 We believe that soulbound token serves as a specialized subset of the existing EIP-721 tokens. The advantage of such design is seamless compatibility of soulbound token with existing NFT services. Service providers can treat SBTs like NFTs and do not need to make drastic changes to their existing codebase. @@ -79,7 +81,7 @@ One problem with current soulbound token implementations that extend from [EIP-7 ### Burn Authorization We want maximum freedom when it comes to interface usage. A flexible and predetermined rule to burn is crucial. Here are some sample scenarios for different burn authorizations: -- `IssuerOnly`: Loan record +- `IssuerOnly`: Loan record - `ReceiverOnly`: Paid membership - `Both`: Credentials - `Neither`: Credit history @@ -96,7 +98,7 @@ A concern Ethereum users have is that soulbound tokens having immutable ownershi This proposal is fully backward compatible with [EIP-721](./eip-721.md) ## Security Considerations -Needs discussion. +There are no security considerations related directly to the implementation of this standard. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5528.md b/EIPS/eip-5528.md index f67b8bd2dab657..a27b857208f239 100644 --- a/EIPS/eip-5528.md +++ b/EIPS/eip-5528.md @@ -5,7 +5,7 @@ description: Allows refunds for EIP-20 tokens by escrow smart contract author: StartfundInc (@StartfundInc) discussions-to: https://ethereum-magicians.org/t/eip-5528-refundable-token-standard/10494 status: Last Call -last-call-deadline: 2022-10-16 +last-call-deadline: 2022-11-09 type: Standards Track category: ERC created: 2022-08-16 @@ -14,40 +14,41 @@ requires: 20 ## Abstract -This standard is an extension of [EIP-20](./eip-20.md). This specification provides a type of escrow service in the blockchain ecosystem, which includes the following capabilities. +This standard is an extension of [EIP-20](./eip-20.md). This specification defines a type of escrow service with the following flow: - The seller issues tokens. -- The seller creates an escrow smart contract with detailed escrow information. The information could include seller token contract address, buyer token contract address, lock period, exchange rate, the maximum number of buyers, minimum balance of buyers, additional escrow success conditions, etc. -- The seller funds seller tokens to the escrow contract. -- Buyers fund buyer tokens which are pre-defined in the escrow contract. +- The seller creates an escrow smart contract with detailed escrow information like contract addresses, lock period, exchange rate, additional escrow success conditions, etc. +- The seller funds seller tokens to the *Escrow Contract*. +- Buyers fund buyer tokens which are pre-defined in the *Escrow Contract*. - When the escrow status meets success, the seller can withdraw buyer tokens, and buyers can withdraw seller tokens based on exchange rates. - Buyers can withdraw (or refund) their funded token if the escrow process is failed or is in the middle of the escrow process. ## Motivation -Due to the nature of cryptocurrencies that guarantee anonymity, there is no way to get it back to the cryptocurrency that has already been paid. +Because of the pseudonymous nature of cryptocurrencies, there is no automatic recourse to recover funds that have already been paid. -To solve this problem, the Escrow service exists in the real world. However, it is challenging to implement an escrow service coordinated by a third-party arbitrator in a decentralized cryptocurrency ecosystem. To solve this, a smart contract was designed that acts as an escrow and devised a function where each token is sent back to the original wallet if the escrow is not completed. - -Escrow smart contract service should support refund EIP-20 tokens in the middle of the escrow process or when the operation fails. +In traditional finance, trusted escrow services solve this problem. In the world of decentralized cryptocurrency, however, it is possible to implement an escrow service without a third-party arbitrator. This standard defines an interface for smart contracts to act as an escrow service with a function where tokens are sent back to the original wallet if the escrow is not completed. ## Specification There are two types of contract for the escrow process: -- `Payable Contract`: The sellers and buyers use this token to fund the `Escrow Contract`. -- `Escrow Contract`: Defines the escrow policies and holds `Payable Contract`'s token for a certain period. - -This standard proposes interfaces on top of the [EIP-20](./eip-20.md) standard. +- *Payable Contract*: The sellers and buyers use this token to fund the *Escrow Contract*. This contract MUST override [EIP-20](./eip-20.md) interfaces. +- *Escrow Contract*: Defines the escrow policies and holds *Payable Contract*'s token for a certain period. This contract does not requires override [EIP-20](./eip-20.md) interfaces. ### Methods -#### constructor +#### `constructor` + +The *Escrow Contract* demonstrates details of escrow policies as none-mutable matter in constructor implementation. + +The *Escrow Contract* MUST define the following policies: -The `Escrow Contract` may define the following policies: +- Seller token contract address +- Buyer token contract address + +The *Escrow Contract* MAY define the following policies: -- MUST include seller token contract address -- MUST include buyer token contract address - Escrow period - Maximum (or minimum) number of investors - Maximum (or minimum) number of tokens to fund @@ -58,17 +59,16 @@ The `Escrow Contract` may define the following policies: Funds `_value` amount of tokens to address `_to`. -In the case of `Escrow Contract`: +In the case of *Escrow Contract*: - `_to` MUST be the user address. - - `msg.sender` MUST be the payable contract address. + - `msg.sender` MUST be the *Payable Contract* address. - MUST check policy validations. -In the case of `Payable Contract`: +In the case of *Payable Contract*: - - The address `_to` MUST be the escrow contract address. - - MUST call EIP-20's `_transfer` likely function. - - Before calling `_transfer` function, MUST call the same function of the escrow contract interface. The parameter `_to` MUST be `msg.sender` to recognize the user address in the escrow contract. + - The address `_to` MUST be the *Escrow Contract* address. + - MUST call the same function of the *Escrow Contract* interface. The parameter `_to` MUST be `msg.sender` to recognize the user address in the *Escrow Contract*. ```solidity function escrowFund(address _to, uint256 _value) public returns (bool) @@ -78,17 +78,16 @@ function escrowFund(address _to, uint256 _value) public returns (bool) Refunds `_value` amount of tokens from address `_from`. -In the case of `Escrow Contract`: +In the case of *Escrow Contract*: - `_from` MUST be the user address. - - `msg.sender` MUST be the payable contract address. + - `msg.sender` MUST be the *Payable Contract* address. - MUST check policy validations. -In the case of `Payable Contract`: +In the case of *Payable Contract*: - - The address `_from` MUST be the escrow contract address. - - MUST call EIP-20's `_transfer` likely function. - - Before calling `_transfer` function, MUST call the same function of the escrow contract interface. The parameter `_from` MUST be `msg.sender` to recognize the user address in the escrow contract. + - The address `_from` MUST be the *Escrow Contract* address. + - MUST call the same function of the *Escrow Contract* interface. The parameter `_from` MUST be `msg.sender` to recognize the user address in the *Escrow Contract*. ```solidity function escrowRefund(address _from, uint256 _value) public returns (bool) @@ -98,11 +97,12 @@ function escrowRefund(address _from, uint256 _value) public returns (bool) Withdraws funds from the escrow account. -In the case of `Escrow Contract`: +In the case of *Escrow Contract*: + - MUST check the escrow process is completed. - MUST send the remaining balance of seller and buyer tokens to `msg.sender`'s seller and buyer contract wallets. -In the case of `Payable Contract`, it is optional. +In the case of *Payable Contract*, it is optional. ```solidity function escrowWithdraw() public returns (bool) @@ -110,49 +110,157 @@ function escrowWithdraw() public returns (bool) ### Example of interface +This example demonstrates simple exchange of one seller and one buyer in one-to-one exchange rates. + ```solidity pragma solidity ^0.4.20; -interface IERC5528 is ERC20 { +interface IERC5528 { function escrowFund(address _to, uint256 _value) public returns (bool); - function escrowRefund(address to, uint256 amount) public returns (bool); + function escrowRefund(address _from, uint256 _value) public returns (bool); function escrowWithdraw() public returns (bool); } +contract PayableContract is IERC5528, IERC20 { + /* + General ERC20 implementations + */ + + function _transfer(address from, address to, uint256 amount) internal { + uint256 fromBalance = _balances[from]; + require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + _balances[from] = fromBalance - amount; + _balances[to] += amount; + } + + function transfer(address to, uint256 amount) public returns (bool) { + address owner = msg.sender; + _transfer(owner, to, amount); + return true; + } + + function escrowFund(address _to, uint256 _value) public returns (bool){ + bool res = IERC5528(to).escrowFund(msg.sender, amount); + require(res, "Fund Failed"); + _transfer(msg.sender, to, amount); + return true; + } + + function escrowRefund(address _from, uint256 _value) public returns (bool){ + bool res = IERC5528(_from).escrowRefund(msg.sender, _value); + require(res, "Refund Failed"); + _transfer(_from, msg.sender, _value); + return true; + } +} + +contract EscrowContract is IERC5528 { + + enum State { Inited, Running, Success, Closed } + struct BalanceData { + address addr; + uint256 amount; + } + + address _addrSeller; + address _addrBuyer; + BalanceData _fundSeller; + BalanceData _fundBuyer; + EscrowStatus _status; + + constructor(address sellerContract, address buyerContract){ + _addrSeller = sellerContract; + _addrBuyer = buyerContract; + _status = State.Inited; + } + + function escrowFund(address _to, uint256 _value) public returns (bool){ + if(msg.sender == _addrSeller){ + require(_status.state == State.Running, "must be running state"); + _fundSeller.addr = _to; + _fundSeller.amount = _value; + _status = State.Success; + }else if(msg.sender == _addrBuyer){ + require(_status.state == State.Inited, "must be init state"); + _fundBuyer.addr = _to; + _fundBuyer.amount = _value; + _status = State.Running; + }else{ + require(false, "Invalid to address"); + } + return true; + } + + function escrowRefund(address _from, uint256 amount) public returns (bool){ + require(_status.state == State.Running, "refund is only available on running state"); + require(msg.sender == _addrBuyer, "invalid caller for refund"); + require(_fundBuyer.addr == _from, "only buyer can refund"); + require(_fundBuyer.amount >= amount, "buyer fund is not enough to refund"); + _fundBuyer.amount = _fundBuyer.amount - amount + return true; + } + + function escrowWithdraw() public returns (bool){ + require(_status.state == State.Success, "withdraw is only available on success state"); + uint256 common = MIN(_fundBuyer.amount, _fundSeller.amount); + + if(common > 0){ + _fundBuyer.amount = _fundBuyer.amount - common; + _fundSeller.amount = _fundSeller.amount - common; + + // Exchange + IERC5528(_addrSeller).transfer(_fundBuyer.addr, common); + IERC5528(_addrBuyer).transfer(_fundSeller.addr, common); + + // send back the remaining balances + if(_fundBuyer.amount > 0){ + IERC5528(_addrBuyer).transfer(_fundBuyer.addr, _fundBuyer.amount); + } + if(_fundSeller.amount > 0){ + IERC5528(_addrSeller).transfer(_fundSeller.addr, _fundSeller.amount); + } + } + + _status = State.Closed; + } + +} + ``` ## Rationale -The interfaces described in this EIP have been chosen to cover the refundable issue in the escrow operation. +The interfaces cover the escrow operation's refundable issue. The suggested 3 functions (`escrowFund`, `escrowRefund` and `escrowWithdraw`) are based on `transfer` function in EIP-20. -`escrowFund` send tokens to the escrow contract. The escrow contract can hold the contract in the escrow process or reject tokens if the policy does not meet. +`escrowFund` send tokens to the *Escrow Contract*. The *Escrow Contract* can hold the contract in the escrow process or reject tokens if the policy does not meet. -`escrowRefund` can be invoked in the middle of the escrow process or when the escrow process is failed. +`escrowRefund` can be invoked in the middle of the escrow process or when the escrow process fails. -`escrowWithdraw` allows users (sellers and buyers) to transfer tokens from the escrow account. When the escrow process is completed, the seller can get the buyer's token, and the buyers can get the seller's token. +`escrowWithdraw` allows users (sellers and buyers) to transfer tokens from the escrow account. When the escrow process completes, the seller can get the buyer's token, and the buyers can get the seller's token. ## Backwards Compatibility -This EIP is fully backward compatible with the [EIP-20](./eip-20.md) specification. +The *Payable Contract* which implements this EIP is fully backward compatible with the [EIP-20](./eip-20.md) specification. ## Test Cases [Unit test example by truffle](../assets/eip-5528/truffule-test.js). This test case demonstrates the following conditions for exchanging seller/buyer tokens. + - The exchange rate is one-to-one. - If the number of buyers reaches 2, the escrow process will be terminated(success). - Otherwise (not meeting success condition yet), buyers can refund (or withdraw) their funded tokens. ## Security Considerations -Since the escrow contract controls seller and buyer rights, flaws within the escrow contract will directly lead to unexpected behavior and potential loss of funds. +Since the *Escrow Contract* controls seller and buyer rights, flaws within the *Escrow Contract* will directly lead to unexpected behavior and potential loss of funds. ## Copyright diff --git a/EIPS/eip-5553.md b/EIPS/eip-5553.md index b510d9d4c8a6f6..b131e89e021991 100644 --- a/EIPS/eip-5553.md +++ b/EIPS/eip-5553.md @@ -4,7 +4,7 @@ title: Representing IP and its Royalty Structure description: A way of representing intellectual property and its respective royalty structure on chain author: Roy Osherove (@royosherove) discussions-to: https://ethereum-magicians.org/t/eip-5553-representing-intellectual-property-on-chain-with-royalty-rights/10551 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-08-17 @@ -12,25 +12,26 @@ requires: 20, 721 --- ## Abstract -This proposal introduces a generic way to represent intellectual property on chain, along with a refined royalty representation mechanism and associated metadata link. This standard is not associated with a specific type of IP and could represent many types of IP such as musical IP, videos, books, images, and more. -The standard is kept very generic on purpose to allow the industry to evolve new ecosystems that can all rely on the same basic standard at their core. +This proposal introduces a generic way to represent intellectual property on chain, along with a refined royalty representation mechanism and associated metadata link. This standard is not associated with a specific type of IP and could represent many types of IP, such as musical IP, videos, books, images, and more. +The standard is kept very generic to allow the industry to evolve new ecosystems that can all rely on the same basic standard at their core. This standard allows market participants to: 1) Observe the canonical on-chain representation of an intellectual property 2) Discover its attached metadata 3) Discover its related royalty structure -4) In the future, this will enable building registration, licensing, and payout mechanisms for intellectual property assets. +4) This will enable building registration, licensing, and payout mechanisms for intellectual property assets in the future. ## Motivation -There is no accepted standard mechanism to license intellectual property or to represent it, except using traditional NFTs. But regular NFTs only represent a collectible item use case, and cannot easily represent more complicated use cases of licensing IP for different types of uses. -To enable such mechanisms, a more robust mechanism is needed to: +There is no accepted standard mechanism to license intellectual property or to represent it, except using traditional NFTs. However, regular NFTs only represent a collectible item use case and cannot easily represent more complicated use cases of licensing IP for different types of uses. +We can enable such licensing mechanisms if we can: + 1) Declare that IP exists, SEPARATELY from its purchase ability 2) Declare possibly multiple interested parties to be paid for such IP For 1, no standard exists today. -For 2, there are regular split standards based on NFT purchases, or through mechanisms like 0xsplits. While these solve the main problem, they do not contain the ability to name multiple types of collaboration participants. +For 2, traditional split standards exist based on NFT purchases or through mechanisms like 0xsplits. While these solve the main problem, they do not contain the ability to name multiple types of collaboration participants. @@ -46,19 +47,19 @@ Implementers of this standard **MUST** have all of the following functions: ### royaltyPortionTokens() function This function MUST return an array of addresses related to [EIP-20](./eip-20.md) tokens that MUST represent royalty portions to different types of interested parties. These royalty portion tokens represent a more granular and streamlined way to declare royalty splits for multiple collaboration participants for the creation of the IP. -For example, for a musical IP, we might have two tokens representing the composition/writing/publishing royalty portion side, and the recording/master side. These royalty portion tokens are distributed to the collaboration participants and can later be queried by the various holders, for purposes of distribution of royalties - i.e if you hold 10% of a royalty portion token, you will get 10% of financial distribution related to that type of royalty. +For example, for a musical IP, we might have two tokens representing the composition/writing/publishing royalty portion side and the recording/master side. These royalty portion tokens are distributed to the collaboration participants and can later be queried by the various holders to distribute royalties. I.e., if one holds 10% of a royalty portion token, that holder will get 10% of the financial distribution related to that type of royalty. ### metadataURI() function -This function MUST return the URI to a metadata file containing any required metadata for the IP, or an empty string. Each IP type MAY implement its metadata standard, defined separately. The file MUST be hosted in IPFS or Arweave or other decentralized content-addressable systems in which the file's contents are not changeable without changing the URI. +This function MUST return the URI to a metadata file containing any required metadata for the IP or an empty string. Each IP type MAY implement its metadata standard, defined separately. The file MUST be hosted in IPFS, Arweave, or other decentralized content-addressable systems in which the file's contents are not changeable without changing the URI. ### changeMetadataURI() function This function allows changing the metadata URI to point to a new version of the metadata file. Calling this function MUST trigger the event `MetadataChanged` in case of success. ### ledger() function -This function MUST return the address of the registry or registrar contract, or an EOA account that initialized the IP and associated royalty tokens. An IP representation MAY be registered in multiple places by different actors for different purposes. This enables market participants to discover which registry mechanism is the parent of the IP and might have special access rights to manage the IP. +This function MUST return the registry or registrar contract address or an EOA account that initialized the IP and associated royalty tokens. An IP representation MAY be registered in multiple places by different actors for different purposes. This function enables market participants to discover which registry mechanism is the parent of the IP and might have special access rights to manage the IP. ```solidity -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.9; import '@openzeppelin/contracts/interfaces/IERC165.sol'; @@ -70,7 +71,7 @@ interface IIPRepresentation is IERC165 { /// @notice Called with the new URI to an updated metadata file /// @param _newUri - the URI pointing to a metadata file (file standard is up to the implementer) - /// @param _newFileHash - The hash of the new metadata file, for future reference and verification + /// @param _newFileHash - The hash of the new metadata file for future reference and verification function changeMetadataURI(string memory _newUri, string memory _newFileHash) external ; /// @return array of addresses of ERC20 tokens representing royalty portion in the IP @@ -78,7 +79,7 @@ interface IIPRepresentation is IERC165 { function royaltyPortionTokens() external view returns (address[] memory) ; /// @return the address of the contract or EOA that initialized the IP registration - /// @dev i.e a registry or registrar, to be implemented in the future + /// @dev i.e., a registry or registrar, to be implemented in the future function ledger() external view returns (address) ; /// @return the URI of the current metadata file for the II P @@ -99,10 +100,10 @@ interface IIPRepresentation is IERC165 { ### Returning an array of EIP-20 tokens presents a more robust royalty portions structure/ -Current royalty implementations deal only with a single type of royalty payment: NFT sales. They also only allow a single type of royalty - i.e you cannot have different royalty portion owners receive a royalty based on different licensing types. -In other words, currently, a royalty split works the same way no matter what type of purchase or license deal has happened, for all parties involved. +Current royalty implementations deal only with a single type of royalty payment: NFT sales. They also only allow a single type of royalty - i.e., Music NFTs cannot pay different people in different scenarios. +In other words, currently, a royalty split works the same way no matter what type of purchase or license deal has happened for all parties involved. -With this proposal, multiple **types** of royalty scenarios are allowed. A classic case is the music industry in which you have writing/composition royalties, and recording/master royalties. Different licensing types will pay different percentages to different parties based on context. +With this proposal, multiple **types** of royalty scenarios are allowed. A classic case is the music industry, in which we have writing/composition royalties and recording/master royalties. Different licensing types will pay different percentages to different parties based on context. In the case of a song cover, a license payment formula can be created so that that a) Original IP's writers get paid for using the lyrics or composition of the song @@ -114,17 +115,17 @@ Moreover, this EIP has a single structure that connects to all types of royalty Lastly, moving EIP-20 tokens around is much easier than managing an 0xsplits contract. ### Separating the IP contract from the collectible and licensing NFTs enables scaling licensing types -By separating the canonical version of the IP from its various licensed uses (NFT purchase, streaming, usage of art, etc..) this EIP introduces a path for an ecosystem of various license types and payment distributions to evolve. -In other words, when people use this scheme, they won't start by creating a music NFT or art NFT, they start by creating the IP Representation and then create types of licenses or collectibles for it, each as its own sellable NFT, possibly in the form of [EIP-5218](./eip-5218.md) or other formats. +By separating the canonical version of the IP from its various licensed uses (NFT purchase, streaming, usage of art and more.), this EIP introduces a path for an ecosystem of various license types and payment distributions to evolve. +In other words, when people use this scheme, they will not start by creating a music NFT or art NFT; they start by creating the IP Representation and then create types of licenses or collectibles for it, each as its own sellable NFT. ### A single pointer to the IP's metadata -The IPR points to metadata housed in IPFS or Arweave and allows changing it and keeping track of the changes in a simple and standard way. Today the only metadata standard is NFT metadata extension, but we do not know which standard the document adheres to. With different IP types, different metadata standards for different IP types can be formulated and have a simple easy place to discover attached metadata. +The IPR points to metadata housed in IPFS or Arweave and allows changing it and keeping track of the changes in a simple and standard way. Today the only metadata standard is NFT metadata extension, but it is impossible to know to which standard the document adheres. With different IP types, different metadata standards for different IP types can be formulated and have a simple, easy place to discover attached metadata. ## Reference Implementation #### Implementing a Musical IP Representation (MIPR for short) based on IIPRepresentation ```solidity -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.9; import '@openzeppelin/contracts/token/ERC721/ERC721.sol'; import "./interfaces/IIPRepresentation.sol"; @@ -212,7 +213,7 @@ contract MusicalIP is ERC721, IIPRepresentation { #### Deploying a new Musical IP using a simple song registry contract ```solidity -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/Counters.sol"; import "./MusicalIP.sol"; @@ -248,8 +249,8 @@ contract SimpleSongLedger is IERC721Receiver { There might be potential security challenges of attackers persuading holders of royalty portion tokens to send them those tokens and gaining royalty portion in various IPRs. However, these are not specific to royalties and are a common issue with EIP-20 tokens. -In the case of the IP registration ownership, it will be recommended that registry contracts will own the IP registration and it will be non-transferrable (account bound to the registry that created it). +In the case of the IP registration ownership, it will be recommended that registry contracts own the IP registration, which will be non-transferrable (account bound to the registry that created it). ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). \ No newline at end of file +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5585.md b/EIPS/eip-5585.md new file mode 100644 index 00000000000000..3dd1e3cde058a6 --- /dev/null +++ b/EIPS/eip-5585.md @@ -0,0 +1,166 @@ +--- +eip: 5585 +title: EIP-721 NFT Authorization +description: Allows NFT owners to authorize other users to use their NFTs. +author: Veega Labs (@VeegaLabsOfficial), Sean NG (@ngveega), Tiger (@tiger0x), Fred (@apan), Fov Cao (@fovcao) +discussions-to: https://ethereum-magicians.org/t/nft-authorization-erc721-extension/10661 +status: Draft +type: Standards Track +category: ERC +created: 2022-08-15 +requires: 721 +--- + +## Abstract + +This EIP separates an [EIP-721](./eip-721.md) NFT's ownership from its commercial usage rights to allow for the independent management of those rights. + +## Motivation + +Most NFTs have a simplified ownership verification mechanism, with a sole owner of an NFT. Under this model, other rights, such as display, or creating derivative works or distribution, are not possible to grant, limiting the value and commercialization of NFTs. Therefore, the separation of an NFT's ownership and user rights can enhance its commercial value. + +Commercial right is a broad concept based on the copyright, including the rights of copy, display, distribution, renting, commercial use, modify, reproduce and sublicense etc. With the development of the Metaverse, NFTs are becoming more diverse, with new use cases such as digital collections, virtual real estate, music, art, social media, and digital asset of all kinds. The copyright and authorization based on NFTs are becoming a potential business form. + +## Specification + +The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY” and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +### Contract Interface + +```solidity +interface IERC5585 { + + struct UserRecord { + address user; + string[] rights; + uint expires + } + + /// @notice NFT holder authorizes all the rights to a user for a specified period of time + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT which is authorized + /// @param user The user to whom the NFT is authorized + /// @param duration The period of time the authorization lasts + function authorizeUser(uint256 tokenId, address user, uint duration) external; + + /// @notice NFT holder authorizes specific rights to a user for a specified period of time + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT which is authorized + /// @param user The user to whom the NFT is authorized + /// @param rights Rights autorised to the user, such as renting, distribution or display etc + /// @param duration The period of time the authorization lasts + function authorizeUser(uint256 tokenId, address user, string[] rights, uint duration) external; + + /// @notice NFT holder extends the duration of authorization + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT which has been authorized + /// @param user The user to whom the NFT has been authorized + /// @param duration The new duration of the authorization + function extendDuration(uint256 tokenId, address user, uint duration) external; + + /// @notice NFT holder updates the rights of authorization + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT which has been authorized + /// @param user The user to whom the NFT has been authorized + /// @param rights New rights autorised to the user + function updateUserRights(uint256 tokenId, address user, string[] rights) external; + + /// @notice Get the authorization expired time of the specified NFT and user + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT to get the user expires for + /// @param user The user who has been authorized + /// @return The authorization expired time + function getExpires(uint256 tokenId, address user) external view returns(uint); + + /// @notice Get the rights of the specified NFT and user + /// @dev The zero address indicates there is no user + /// @param tokenId The NFT to get the rights + /// @param user The user who has been authorized + /// @return The rights has been authorized + function getUserRights(uint256 tokenId, address user) external view returns(string[]); + + /// @notice The contract owner can update the number of users that can be authorized per NFT + /// @param userLimit The number of users set by operators only + function updateUserLimit(unit256 userLimit) external onlyOwner; + + /// @notice resetAllowed flag can be updated by contract owner to control whether the authorization can be revoked or not + /// @param resetAllowed It is the boolean flag + function updateResetAllowed(bool resetAllowed) external onlyOwner; + + /// @notice Check if the token is available for authorization + /// @dev Throws if tokenId is not a valid NFT + /// @param tokenId The NFT to be checked the availability + /// @return true or false whether the NFT is available for authorization or not + function checkAuthorizationAvailability(uint256 tokenId) public view returns(bool); + + /// @notice Clear authorization of a specified user + /// @dev The zero address indicates there is no user. The function works when resetAllowed is true and it will throw exception when false + /// @param tokenId The NFT on which the authorization based + /// @param user The user whose authorization will be cleared + function resetUser(uint256 tokenId, address user) external; + + /// @notice This is an OPTIONAL function that the operator MAY call, he can set the starting time of staking as a reward of the authorization for each user + /// @dev The zero address indicates there is no user + /// @param user To which user the staking time will be set + /// @param stakingTime The starting time of the staking for each user + function updateStakingTime(address[] user, uint[] stakingTime) external; + + + /// @notice Emitted when the user of a NFT is changed or the authorization expires time is updated + /// param tokenId The NFT on which the authorization based + /// param indexed user The user to whom the NFT authorized + /// @param rights Rights autorised to the user + /// param expires The expires time of the authorization + event authorizeUser(uint256 indexed tokenId, address indexed user, string[] rights, uint expires); +} +``` + +The `authorizeUser(uint256 tokenId, address user, uint duration)` function MAY be implemented as `public` or `external`. + +The `authorizeUser(uint256 tokenId, address user, string[] rights; uint duration)` function MAY be implemented as `public` or `external`. + +The `extendDuration(uint256 tokenId, address user, uint duration)` function MAY be implemented as `public` or `external`. + +The `updateUserRights(uint256 tokenId, address user, string[] rights)` function MAY be implemented as `public` or `external`. + +The `getExpires(uint256 tokenId, address user)` function MAY be implemented as `pure` or `view`. + +The `getUserRights(uint256 tokenId, address user)` function MAY be implemented as pure and view. + +The `updateUserLimit(unit256 userLimit)` function MAY be implemented as`public` or `external`. + +The `updateResetAllowed(bool resetAllowed)` function MAY be implemented as `public` or `external`. + +The `checkAuthorizationAvailability(uint256 tokenId)` function MAY be implemented as `pure` or `view`. + +The `resetUser(uint256 tokenId, address user)` function MAY be implemented as `public` or `external`. + +The `updateStakingTime(address[] user, uint[] stakingTime)` function MAY be implemented as `public` or `external`. + +The `authorizeUser` event MUST be emittedwhen the user of a NFT is changed or the authorization expires time is updated. + +## Rationale + +First of all, NFT contract owner can set the maximum number of authorized users to each NFT and whether the NFT owner can cancel the authorization at any time to protect the interests of the parties involved. + +Secondly, this EIP combines the functions of staking and authorization, which means the NFT contract owner can update the number of authorized users to NFT owners depending on the period of staking. The function is optional, but it is a way to protect all parties from overhype and to ensure that the price of the NFT is more accurately to match its value. + +Thirdly, there is a resetAllowed flag to control the rights between the NFT owner and the users for the contract owner. If the flag is set to true, then the NFT owner can disable usage rights of all authorized users at any time. + +Fourthly, the rights within the user record struct is used to store what rights has been authorized to a user by the NFT owner, in other words, the NFT owner can authorize a user with specific rights and update it when necessary. + +Finally, this design can be seamlessly integrated with third parties. It is an extension of EIP-721, therefore it can be easily integrated into a new NFT project. Other projects can directly interact with these interfaces and functions to implement their own types of transactions. For example, an announcement platform could use this EIP to allow all NFT owners to make authorization or deauthorization at any time. + +## Backwards Compatibility + +This standard is compatible with [EIP-721](./eip-721.md) since it is an extension of it. + +## Security Considerations + +If someone buys an NFT within the duration of an authorization, they will not have to stake anything, providing no incentive to cancel the authorization. + +To solve this problem, the authorization fee paid by the users will be held in an escrow contract for a period of time depending on the duration of the authorization. For example, if the authorization duration is 12 months and the fee in total is 10 ETH, then if the NFT is transferred after 3 months, then only 2.5 ETH would be sent and the remaining 7.5 ETH would be refunded. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5604.md b/EIPS/eip-5604.md index 658dba9bdb98f5..fe372d8472121b 100644 --- a/EIPS/eip-5604.md +++ b/EIPS/eip-5604.md @@ -2,7 +2,7 @@ eip: 5604 title: NFT Lien description: Extend EIP-721 to support liens -author: Zainan Victor Zhou (@xinbenlv) +author: Zainan Victor Zhou (@xinbenlv), Allen Zhou , Alex Qin discussions-to: https://ethereum-magicians.org/t/creating-a-new-erc-proposal-for-nft-lien/10683 status: Draft type: Standards Track diff --git a/EIPS/eip-5606.md b/EIPS/eip-5606.md index e28a9f43d32aa8..6d24a30fe1ca3e 100644 --- a/EIPS/eip-5606.md +++ b/EIPS/eip-5606.md @@ -44,16 +44,14 @@ interface IMultiverseNFT { address contractAddress; uint256 tokenId; uint256 quantity; - bool isBundled; - address ownerAddress; } /** * @dev Emitted when one or more new delegate NFTs are added to a Multiverse NFT - * */ event Bundled(uint256 multiverseTokenID, DelegateData[] delegateData, address ownerAddress); + /** * @dev Emitted when one or more delegate NFTs are removed from a Multiverse NFT */ @@ -75,10 +73,10 @@ interface IMultiverseNFT { * This function accepts the delegate NFT details and transfers those NFTs to the Multiverse NFT contract * Need to ensure that approval is given to this Multiverse NFT contract for the delegate NFTs so that they can be transferred programmatically */ - function bundle(DelegateData[] memory delegateData, uint256 multiverseTokenID, address ownerAddress) external; + function bundle(DelegateData[] memory delegateData, uint256 multiverseTokenID) external; /** - * @dev Initializes a new bundle, mints a Multiverse NFT and assigns it to msg.sender + * @dev Initialises a new bundle, mints a Multiverse NFT and assigns it to msg.sender * Returns the token ID of a new Multiverse NFT * Note - When a new Multiverse NFT is initialised, it is empty; it does not contain any delegate NFTs */ diff --git a/EIPS/eip-5643.md b/EIPS/eip-5643.md index f408c1907bdb03..25c70f7065b450 100644 --- a/EIPS/eip-5643.md +++ b/EIPS/eip-5643.md @@ -4,7 +4,7 @@ title: Subscription NFTs description: Add subscription-based functionality to EIP-721 tokens author: cygaar (@cygaar) discussions-to: https://ethereum-magicians.org/t/eip-5643-subscription-nfts/10802 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-09-10 @@ -30,15 +30,14 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity interface IERC5643 { /// @notice Emitted when a subscription expiration changes - /// The zero address for subscriber indicates that the subscription was canceled. /// @dev When a subscription is canceled, the expiration value should also be 0. event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration); /// @notice Renews the subscription to an NFT /// Throws if `tokenId` is not a valid NFT /// @param tokenId The NFT to renew the subscription for - /// @param expiration The unix timestamp to extend the subscription to - function renewSubscription(uint256 tokenId, uint64 expiration) external payable; + /// @param duration The number of seconds to extend a subscription for + function renewSubscription(uint256 tokenId, uint64 duration) external payable; /// @notice Cancels the subscription of an NFT /// @dev Throws if `tokenId` is not a valid NFT @@ -47,7 +46,6 @@ interface IERC5643 { /// @notice Gets the expiration date of a subscription /// @dev Throws if `tokenId` is not a valid NFT - /// @dev The zero address indicates a subscription does not exist /// @param tokenId The NFT to get the expiration date of /// @return The expiration date of the subscription function expiresAt(uint256 tokenId) external view returns(uint64); @@ -64,7 +62,7 @@ The `expiresAt(uint256 tokenId)` function MAY be implemented as `pure` or `view` The `isRenewable(uint256 tokenId)` function MAY be implemented as `pure` or `view`. -The `renewSubscription(uint256 tokenId)` function MAY be implemented as `external` or `public`. +The `renewSubscription(uint256 tokenId, uint64 duration)` function MAY be implemented as `external` or `public`. The `cancelSubscription(uint256 tokenId)` function MAY be implemented as `external` or `public`. @@ -109,7 +107,7 @@ contract ERC5643Mock is ERC5643 { } } -contract ContractTest is Test { +contract ERC5643Test is Test { event SubscriptionUpdate(uint256 indexed tokenId, uint64 expiration); address user1; @@ -125,9 +123,10 @@ contract ContractTest is Test { } function testRenewalValid() public { + vm.warp(1000); vm.prank(user1); vm.expectEmit(true, true, false, true); - emit SubscriptionUpdate(tokenId, 2000); + emit SubscriptionUpdate(tokenId, 3000); erc5643.renewSubscription(tokenId, 2000); } @@ -149,10 +148,12 @@ contract ContractTest is Test { } function testExpiresAt() public { + vm.warp(1000); + assertEq(erc5643.expiresAt(tokenId), 0); vm.startPrank(user1); erc5643.renewSubscription(tokenId, 2000); - assertEq(erc5643.expiresAt(tokenId), 2000); + assertEq(erc5643.expiresAt(tokenId), 3000); erc5643.cancelSubscription(tokenId); assertEq(erc5643.expiresAt(tokenId), 0); @@ -172,24 +173,37 @@ import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./IERC5643.sol"; contract ERC5643 is ERC721, IERC5643 { - mapping(uint256 => uint64) private _subscriptions; + mapping(uint256 => uint64) private _expirations; constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_) {} - function renewSubscription(uint256 tokenId, uint64 expiration) external payable { + function renewSubscription(uint256 tokenId, uint64 duration) external payable { require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved"); - _subscriptions[tokenId] = expiration; - emit SubscriptionUpdate(tokenId, expiration); + + uint64 currentExpiration = _expirations[tokenId]; + uint64 newExpiration; + if (currentExpiration == 0) { + newExpiration = uint64(block.timestamp) + duration; + } else { + if (!_isRenewable(tokenId)) { + revert SubscriptionNotRenewable(); + } + newExpiration = currentExpiration + duration; + } + + _expirations[tokenId] = newExpiration; + + emit SubscriptionUpdate(tokenId, newExpiration); } function cancelSubscription(uint256 tokenId) external payable { require(_isApprovedOrOwner(msg.sender, tokenId), "Caller is not owner nor approved"); - delete _subscriptions[tokenId]; + delete _expirations[tokenId]; emit SubscriptionUpdate(tokenId, 0); } function expiresAt(uint256 tokenId) external view returns(uint64) { - return _subscriptions[tokenId]; + return _expirations[tokenId]; } function isRenewable(uint256 tokenId) external pure returns(bool) { diff --git a/EIPS/eip-5679.md b/EIPS/eip-5679.md index 283327537e1fdd..76bcc1fabb23ab 100644 --- a/EIPS/eip-5679.md +++ b/EIPS/eip-5679.md @@ -1,10 +1,11 @@ --- eip: 5679 title: Token Minting and Burning -description: An extension for minting and burning tokens for EIP-20, EIP-721, EIP-1155. +description: An extension for minting and burning EIP-20, EIP-721, and EIP-1155 tokens author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc-5679-mint-and-burn-tokens/10913 -status: Review +status: Last Call +last-call-deadline: 2022-11-10 type: Standards Track category: ERC created: 2022-09-17 @@ -12,9 +13,11 @@ requires: 20, 165, 721, 1155 --- ## Abstract + This EIP introduces a consistent way to extend token standards for minting and burning. ## Motivation + Minting and Burning are typical actions for creating and destroying tokens. By establishing a consistent way to mint and burn a token, we complete the basic lifecycle. @@ -26,6 +29,7 @@ Therefore, creating separate methods for burning and minting simplifies implemen and reduces security error. ## Specification + The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. 1. Any contract complying with [EIP-20](./eip-20.md) when extended with this EIP, @@ -34,8 +38,8 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity // The EIP-165 identifier of this interface is 0xa73fd2a4 interface IERC5679Ext20 { - function mint(address _to, uint256 _amount, bytes[] calldata _data) external; - function burn(address _from, uint256 _amount, bytes[] calldata _data) external; + function mint(address _to, uint256 _amount, bytes calldata _data) external; + function burn(address _from, uint256 _amount, bytes calldata _data) external; } ``` @@ -45,8 +49,8 @@ interface IERC5679Ext20 { ```solidity // The EIP-165 identifier of this interface is 0x0346d398 interface IERC5679Ext721 { - function safeMint(address _to, uint256 _id, bytes[] calldata _data) external; - function burn(address _from, uint256 _id, bytes[] calldata _data) external; + function safeMint(address _to, uint256 _id, bytes calldata _data) external; + function burn(address _from, uint256 _id, bytes calldata _data) external; } ``` @@ -56,25 +60,27 @@ interface IERC5679Ext721 { ```solidity // The EIP-165 identifier of this interface is 0xdff2db76 interface IERC5679Ext1155 { - function safeMint(address _to, uint256 _id, uint256 _amount, bytes[] calldata _data) external; - function safeMintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) external; + function safeMint(address _to, uint256 _id, uint256 _amount, bytes calldata _data) external; + function safeMintBatch(address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external; function burn(address _from, uint256 _id, uint256 _amount, bytes[] calldata _data) external; - function burnBatch(address _from, uint256[] memory ids, uint256[] memory amounts, bytes[] calldata _data) external; + function burnBatch(address _from, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata _data) external; } ``` 4. When the token is being minted, the transfer events **MUST** be emitted as if the token in the `_amount` for EIP-20 and EIP-1155 and token id being `_id` for EIP-721 and EIP-1155 -were transfer from address `0x0` to the recipient address identified by `_to`. +were transferred from address `0x0` to the recipient address identified by `_to`. The total supply **MUST** increase accordingly. 5. When the token is being burned, the transfer events **MUST** be emitted as if the token in the `_amount` for EIP-20 and EIP-1155 and token id being `_id` for EIP-721 and EIP-1155 -were transfer from the recipient address identified by `_to` to the address of `0x0`. +were transferred from the recipient address identified by `_to` to the address of `0x0`. The total supply **MUST** decrease accordingly. -6. For EIP-721, the `safeMint` **MUST** operate with respect to the `ERC721TokenReceiver` hook functions. -For EIP-1155, the `safeMint` and `safeMintBatch` **MUST** operate with respect to the `ERC1155TokenReceiver` hook functions. +6. `safeMint` MUST implement the same receiver restrictions as `safeTransferFrom` as defined in +[EIP-721](./eip-721.md) and [EIP-1155](./eip-1155.md). + +7. It's RECOMMENDED for the client to implement [EIP-165](./eip-165.md) identifiers as specified above. ## Rationale @@ -102,17 +108,18 @@ This EIP is designed to be compatible for EIP-20, EIP-721 and EIP-1155 respectiv This EIP depends on the security soundness of the underlying book keeping behavior of the token implementation. In particular, a token contract should carefully design the access control for which role is granted permission -to mint a new token. Failing to safe guard such behavior can cause fraudulant issuance and an elevation of total supply. +to mint a new token. Failing to safe guard such behavior can cause fraudulent issuance and an elevation of total supply. The burning should also carefully design the access control. Typically only the following two roles are entitled to burn a token: - Role 1. The current token holder - Role 2. An role with special privilege. -Either Role 1 OR Role 2 or a consensus between the two are entitled to conduct the burning action. +Either Role 1 OR Role 2 or a consensus between the two are entitled to conduct the burning action. However as author of this EIP we do recognize there are potentially other use case where a third type of role shall be entitled to burning. We keep this EIP less opinionated in such restriction but implementors should be cautious about designing the restriction. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5727.md b/EIPS/eip-5727.md new file mode 100644 index 00000000000000..aae92a5f99355b --- /dev/null +++ b/EIPS/eip-5727.md @@ -0,0 +1,626 @@ +--- +eip: 5727 +title: Semi-Fungible Soulbound Token +description: An interface for soulbound tokens, also known as badges or account-bound tokens, that can be both fungible and non-fungible. +author: Austin Zhu (@AustinZhu), Terry Chen +discussions-to: https://ethereum-magicians.org/t/eip-5727-semi-fungible-soulbound-token/11086 +status: Draft +type: Standards Track +category: ERC +created: 2022-09-28 +requires: 165 +--- + +## Abstract +An interface for soulbound tokens (SBT), which are non-transferable tokens representing a person's identity, credentials, affiliations, reputation, and private assets. + +Our interface can handle a combination of fungible and non-fungible tokens in an organized way. It provides a set of core methods that can be used to manage the lifecycle of soulbound tokens, as well as a rich set of extensions that enables DAO governance, privacy protection, token expiration, and account recovery. + +This interface aims to provide a flexible and extensible framework for the development of soulbound token systems. + +## Motivation +The Web3 ecosystem nowadays is largely dominated by highly-financialized tokens, which are designed to be freely transferable and interchangeable. However, there are many use cases in our society that requires non-transferablity. For example, a membership card guarantees one's proprietary rights in a community, and such rights should not be transferable to others. + +We have already seen many attempts to create such non-transferable tokens in the Ethereum community. However, most of them rely heavily on NFT standards like [EIP-721](./eip-721.md), which are not designed for non-transferability. Others lack the flexibility to support both fungible and non-fungible tokens and do not provide extensible features for critical use cases. + +Our interface can be used to represent non-transferable ownerships, and provides features for common use cases including but not limited to: + +- granular lifecycle management of SBTs (e.g. minting, revocation, expiration) +- management of SBTs via community voting and delegation (e.g. DAO governance, operators) +- recovery of SBTs (e.g. switching to a new wallet) +- token visibility control (e.g. private SBTs, hiding negative tokens) +- fungible and non-fungible SBTs (e.g. membership card and loyalty points) +- the grouping of SBTs using slots (e.g. complex reward schemes with a combination of vouchers, points, and badges) + +A common interface for soulbound tokens will not only help enrich the Web3 ecosystem but also facilitates the growth of a decentralized society. + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +A token is identified by its `tokenId`, which is a 256-bit unsigned integer. A token can also have a value denoting its denomination. + +A slot is identified by its `slotId`, which is a 256-bit unsigned integer. Slots are used to group fungible and non-fungible tokens together, thus make tokens semi-fungible. A token can only belong to one slot at a time. + +### Core +The core methods are used to manage the lifecycle of SBTs. They MUST be supported by all semi-fungible SBT implementations. + +```solidity +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC5727 Soulbound Token Interface + * @dev The core interface. It allows basic query of information about tokens and slots. + * @dev interfaceId = 0x35f61d8a + */ +interface IERC5727 is IERC165 { + /** + * @dev MUST emit when a token is minted. + * @param soul The address that the token is minted to + * @param tokenId The token minted + * @param value The value of the token minted + */ + event Minted(address indexed soul, uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is revoked. + * @param soul The owner soul of the revoked token + * @param tokenId The revoked token + */ + event Revoked(address indexed soul, uint256 indexed tokenId); + + /** + * @dev MUST emit when a token is charged. + * @param tokenId The token to charge + * @param value The value to charge + */ + event Charged(uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is consumed. + * @param tokenId The token to consume + * @param value The value to consume + */ + event Consumed(uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is destroyed. + * @param soul The owner soul of the destroyed token + * @param tokenId The token to destroy. + */ + event Destroyed(address indexed soul, uint256 indexed tokenId); + + /** + * @dev MUST emit when the slot of a token is set or changed. + * @param tokenId The token of which slot is set or changed + * @param oldSlot The previous slot of the token + * @param newSlot The updated slot of the token + */ + event SlotChanged( + uint256 indexed tokenId, + uint256 indexed oldSlot, + uint256 indexed newSlot + ); + + /** + * @notice Get the value of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the balance + * @return The value of `tokenId` + */ + function valueOf(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get the slot of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the slot + * @return The slot of `tokenId` + */ + function slotOf(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get the owner soul of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the owner soul + * @return The address of the owner soul of `tokenId` + */ + function soulOf(uint256 tokenId) external view returns (address); + + /** + * @notice Get the validity of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the validity + * @return If the token is valid + */ + function isValid(uint256 tokenId) external view returns (bool); + + /** + * @notice Get the issuer of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the issuer + * @return The address of the issuer of `tokenId` + */ + function issuerOf(uint256 tokenId) external view returns (address); +} +``` + +### Extensions +All extensions below are OPTIONAL for [EIP-5727](./eip-5727.md) implementations. An implementation MAY choose to implement some, none, or all of them. +#### Enumerable +This extension provides methods to enumerate the tokens of a soul. It is recommended to be implemented together with the core interface. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Enumerable Interface + * @dev This extension allows querying the tokens of a soul. + * @dev interfaceId = 0x211ec300 + */ +interface IERC5727Enumerable is IERC5727 { + /** + * @notice Get the total number of tokens emitted. + * @return The total number of tokens emitted + */ + function emittedCount() external view returns (uint256); + + /** + * @notice Get the total number of souls. + * @return The total number of souls + */ + function soulsCount() external view returns (uint256); + + /** + * @notice Get the tokenId with `index` of the `soul`. + * @dev MUST revert if the `index` exceed the number of tokens owned by the `soul`. + * @param soul The soul whose token is queried for. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenOfSoulByIndex(address soul, uint256 index) + external + view + returns (uint256); + + /** + * @notice Get the tokenId with `index` of all the tokens. + * @dev MUST revert if the `index` exceed the total number of tokens. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenByIndex(uint256 index) external view returns (uint256); + + /** + * @notice Get the number of tokens owned by the `soul`. + * @dev MUST revert if the `soul` does not have any token. + * @param soul The soul whose balance is queried for + * @return The number of tokens of the `soul` + */ + function balanceOf(address soul) external view returns (uint256); + + /** + * @notice Get if the `soul` owns any valid tokens. + * @param soul The soul whose valid token infomation is queried for + * @return if the `soul` owns any valid tokens + */ + function hasValid(address soul) external view returns (bool); +} +``` + +#### Metadata +This extension provides methods to fetch the metadata of a token, a slot and the contract itself. It is recommended to be implemented if you need to specify the appearance and properties of tokens, slots and the contract (i.e. the SBT collection). + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Metadata Interface + * @dev This extension allows querying the metadata of soulbound tokens. + * @dev interfaceId = 0xba3e1a9d + */ +interface IERC5727Metadata is IERC5727 { + /** + * @notice Get the name of the contract. + * @return The name of the contract + */ + function name() external view returns (string memory); + + /** + * @notice Get the symbol of the contract. + * @return The symbol of the contract + */ + function symbol() external view returns (string memory); + + /** + * @notice Get the URI of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token whose URI is queried for + * @return The URI of the `tokenId` token + */ + function tokenURI(uint256 tokenId) external view returns (string memory); + + /** + * @notice Get the URI of the contract. + * @return The URI of the contract + */ + function contractURI() external view returns (string memory); + + /** + * @notice Get the URI of a slot. + * @dev MUST revert if the `slot` does not exist. + * @param slot The slot whose URI is queried for + * @return The URI of the `slot` + */ + function slotURI(uint256 slot) external view returns (string memory); +} +``` + +#### Governance +This extension provides methods to manage the mint and revocation permissions through voting. It is useful if you want to rely on a group of voters to decide the issuance a particular SBT. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Governance Interface + * @dev This extension allows minting and revocation of tokens by community voting. + * @dev interfaceId = 0x3ba738d1 + */ +interface IERC5727Governance is IERC5727 { + /** + * @notice Get the voters of the contract. + * @return The array of the voters + */ + function voters() external view returns (address[] memory); + + /** + * @notice Approve to mint the token described by the `approvalRequestId` to `soul`. + * @dev MUST revert if the caller is not a voter. + * @param soul The soul which the token to mint to + * @param approvalRequestId The approval request describing the value and slot of the token to mint + */ + function approveMint(address soul, uint256 approvalRequestId) external; + + /** + * @notice Approve to revoke the `tokenId`. + * @dev MUST revert if the `tokenId` does not exist. + * @param tokenId The token to revert + */ + function approveRevoke(uint256 tokenId) external; + + /** + * @notice Create an approval request describing the `value` and `slot` of a token. + * @dev MUST revert when `value` is zero. + * @param value The value of the approval request to create + */ + function createApprovalRequest(uint256 value, uint256 slot) external; + + /** + * @notice Remove `approvalRequestId` approval request. + * @dev MUST revert if the caller is not the creator of the approval request. + * @param approvalRequestId The approval request to remove + */ + function removeApprovalRequest(uint256 approvalRequestId) external; + + /** + * @notice Add a new voter `newVoter`. + * @dev MUST revert if the caller is not an administrator. + * MUST revert if `newVoter` is already a voter. + * @param newVoter the new voter to add + */ + function addVoter(address newVoter) external; + + /** + * @notice Remove the `voter` from the contract. + * @dev MUST revert if the caller is not an administrator. + * MUST revert if `voter` is not a voter. + * @param voter the voter to remove + */ + function removeVoter(address voter) external; +} +``` + +#### Delegate +This extension provides methods to delegate a one-time mint and revocation right to an operator. It is useful if you want to temporarily allow an operator to mint and revoke tokens on your behalf. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Delegate Interface + * @dev This extension allows delegation of (batch) minting and revocation of tokens to operator(s). + * @dev interfaceId = 0x3da384b4 + */ +interface IERC5727Delegate is IERC5727 { + /** + * @notice Delegate a one-time minting right to `operator` for `delegateRequestId` delegate request. + * @dev MUST revert if the caller does not have the right to delegate. + * @param operator The soul to which the minting right is delegated + * @param delegateRequestId The delegate request describing the soul, value and slot of the token to mint + */ + function mintDelegate(address operator, uint256 delegateRequestId) external; + + /** + * @notice Delegate one-time minting rights to `operators` for corresponding delegate request in `delegateRequestIds`. + * @dev MUST revert if the caller does not have the right to delegate. + * MUST revert if the length of `operators` and `delegateRequestIds` do not match. + * @param operators The souls to which the minting right is delegated + * @param delegateRequestIds The delegate requests describing the soul, value and slot of the tokens to mint + */ + function mintDelegateBatch( + address[] memory operators, + uint256[] memory delegateRequestIds + ) external; + + /** + * @notice Delegate a one-time revoking right to `operator` for `tokenId` token. + * @dev MUST revert if the caller does not have the right to delegate. + * @param operator The soul to which the revoking right is delegated + * @param tokenId The token to revoke + */ + function revokeDelegate(address operator, uint256 tokenId) external; + + /** + * @notice Delegate one-time minting rights to `operators` for corresponding token in `tokenIds`. + * @dev MUST revert if the caller does not have the right to delegate. + * MUST revert if the length of `operators` and `tokenIds` do not match. + * @param operators The souls to which the revoking right is delegated + * @param tokenIds The tokens to revoke + */ + function revokeDelegateBatch( + address[] memory operators, + uint256[] memory tokenIds + ) external; + + /** + * @notice Mint a token described by `delegateRequestId` delegate request as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param delegateRequestId The delegate requests describing the soul, value and slot of the token to mint. + */ + function delegateMint(uint256 delegateRequestId) external; + + /** + * @notice Mint tokens described by `delegateRequestIds` delegate request as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param delegateRequestIds The delegate requests describing the soul, value and slot of the tokens to mint. + */ + function delegateMintBatch(uint256[] memory delegateRequestIds) external; + + /** + * @notice Revoke a token as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param tokenId The token to revoke. + */ + function delegateRevoke(uint256 tokenId) external; + + /** + * @notice Revoke multiple tokens as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param tokenIds The tokens to revoke. + */ + function delegateRevokeBatch(uint256[] memory tokenIds) external; + + /** + * @notice Create a delegate request describing the `soul`, `value` and `slot` of a token. + * @param soul The soul of the delegate request. + * @param value The value of the delegate request. + * @param slot The slot of the delegate request. + * @return delegateRequestId The id of the delegate request + */ + function createDelegateRequest( + address soul, + uint256 value, + uint256 slot + ) external returns (uint256 delegateRequestId); + + /** + * @notice Remove a delegate request. + * @dev MUST revert if the delegate request does not exists. + * MUST revert if the caller is not the creator of the delegate request. + * @param delegateRequestId The delegate request to remove. + */ + function removeDelegateRequest(uint256 delegateRequestId) external; +} +``` + +#### Recovery +This extension provides methods to recover tokens from a stale soul. It is recommended to use this extension so that users are able to retrieve their tokens from a compromised or old wallet in certain situations. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Recovery Interface + * @dev This extension allows recovering soulbound tokens from an address provided its signature. + * @dev interfaceId = 0x379f4e66 + */ +interface IERC5727Recovery is IERC5727 { + /** + * @notice Recover the tokens of `soul` with `signature`. + * @dev MUST revert if the signature is invalid. + * @param soul The soul whose tokens are recovered + * @param signature The signature signed by the `soul` + */ + function recover(address soul, bytes memory signature) external; +} +``` +#### Expirable +This extension provides methods to manage the expiration of tokens. It is useful if you want to expire/invalidate tokens after a certain period of time. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Expirable Interface + * @dev This extension allows soulbound tokens to be expired. + * @dev interfaceId = 0x2a8cf5aa + */ +interface IERC5727Expirable is IERC5727 { + /** + * @notice Get the expire date of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token for which the expiry date is queried + * @return The expiry date of the token + */ + function expiryDate(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get if a token is expired. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token for which the expired status is queried + * @return If the token is expired + */ + function isExpired(uint256 tokenId) external view returns (bool); + + /** + * @notice Set the expiry date of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * MUST revert if the `date` is in the past. + * @param tokenId The token whose expiry date is set + * @param date The expire date to set + */ + function setExpiryDate(uint256 tokenId, uint256 date) external; + + /** + * @notice Set the expiry date of multiple tokens. + * @dev MUST revert if the `tokenIds` tokens does not exist. + * MUST revert if the `dates` is in the past. + * MUST revert if the length of `tokenIds` and `dates` do not match. + * @param tokenIds The tokens whose expiry dates are set + * @param dates The expire dates to set + */ + function setBatchExpiryDates( + uint256[] memory tokenIds, + uint256[] memory dates + ) external; +} +``` + +#### Shadow +This extension provides methods to manage the visibility of tokens. It is useful if you want to hide tokens that you don't want to show to the public. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Shadow Interface + * @dev This extension allows restricting the visibility of specific soulbound tokens. + * @dev interfaceId = 0x3475cd68 + */ +interface IERC5727Shadow is IERC5727 { + /** + * @notice Shadow a token. + * @dev MUST revert if the `tokenId` token does not exists. + * @param tokenId The token to shadow + */ + function shadow(uint256 tokenId) external; + + /** + * @notice Reveal a token. + * @dev MUST revert if the `tokenId` token does not exists. + * @param tokenId The token to reveal + */ + function reveal(uint256 tokenId) external; +} +``` + +#### SlotEnumerable +This extension provides methods to enumerate slots. A slot is used to group tokens that share similar utility and properties. + +```solidity +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; +import "./IERC5727Enumerable.sol"; + +/** + * @title ERC5727 Soulbound Token Slot Enumerable Interface + * @dev This extension allows querying information about slots. + * @dev interfaceId = 0x3b741b9e + */ +interface IERC5727SlotEnumerable is IERC5727, IERC5727Enumerable { + /** + * @notice Get the total number of slots. + * @return The total number of slots. + */ + function slotCount() external view returns (uint256); + + /** + * @notice Get the slot with `index` among all the slots. + * @dev MUST revert if the `index` exceed the total number of slots. + * @param index The index of the slot queried for + * @return The slot is queried for + */ + function slotByIndex(uint256 index) external view returns (uint256); + + /** + * @notice Get the number of tokens in a slot. + * @dev MUST revert if the slot does not exist. + * @param slot The slot whose number of tokens is queried for + * @return The number of tokens in the `slot` + */ + function tokenSupplyInSlot(uint256 slot) external view returns (uint256); + + /** + * @notice Get the tokenId with `index` of the `slot`. + * @dev MUST revert if the `index` exceed the number of tokens in the `slot`. + * @param slot The slot whose token is queried for. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenInSlotByIndex(uint256 slot, uint256 index) + external + view + returns (uint256); +} +``` + +## Rationale +### Token storage model +We adopt semi-fungible token storage models designed to support both fungible and non-fungible tokens, inspired by the semi-fungible token standard. We found that such a model is better suited to the representation of SBT than the model used in [EIP-1155](./eip-1155.md). + +Firstly, each slot can be used to represent different categories of SBTs. For instance, a DAO can have membership SBTs, role badges, scores, etc. in one SBT collection. + +Secondly, unlike [EIP-1155](./eip-1155.md), in which each unit of fungible tokens is exactly the same, our interface can help differentiate between similar tokens. This is justified by that credential scores obtained from different entities differ not only in value but also in their effects, validity periods, origins, etc. However, they still share the same slot as they all contribute to a person's credibility, membership, etc. + +### Recovery mechanism +To prevent the loss of SBTs, we propose a recovery mechanism that allows users to recover their tokens by providing a signature signed by their soul address. This mechanism is inspired by [EIP-1271](./eip-1271.md). + +Since SBTs are bound to an address and are meant to represent the identity of the address, which cannot be split into fractions. Therefore, each recovery should be considered as a transfer of all the tokens of the owner. This is why we use the `recover` function instead of `transferFrom` or `safeTransferFrom`. + +### Token visibility control +Our interface allows users to control the visibility of their tokens (shadowing and revealing). This is useful when a user wants to hide some of their tokens from the public, for example, when they want to keep their membership secret. Generally, the issuer and the owner of the token have access to the token by default and can control the visibility of the token. After the token is shadowed, information about the token (e.g. token URI, owner of the token) cannot be queried by the public. + +## Backwards Compatibility +This EIP proposes a new token interface which is meant to be used standalone, and is not backwards compatible with [EIP-721](./eip-721.md), [EIP-1155](./eip-1155.md), [EIP-3525](./eip-3525.md) or any other token standards. However, the naming style of functions and arguments follows the convention of [EIP-721](./eip-721.md) and [EIP-3525](./eip-3525.md), so that developers can understand the intentions easily. + +This EIP is compatible with [EIP-165](./eip-165.md). + +## Test Cases +Our sample implementation includes test cases written using Hardhat. + +## Reference Implementation +You can find our sample implementation [here](../assets/eip-5727/contracts/ERC5727Example.sol). + +## Security Considerations +This EIP does not involve the general transfer of tokens, and thus there will be no security issues related to token transfer generally. + +However, users should be aware of the security risks of using the recovery mechanism. If a user loses his/her private key, all his/her soulbound tokens will be exposed to potential theft. The attacker can create a signature and restore all SBTs of the victim. Therefore, users should always keep their private keys safe. We recommend developers implement a recovery mechanism that requires multiple signatures to restore SBTs. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5732.md b/EIPS/eip-5732.md new file mode 100644 index 00000000000000..3964ab89129d39 --- /dev/null +++ b/EIPS/eip-5732.md @@ -0,0 +1,202 @@ +--- +eip: 5732 +title: Commit Interface +description: A simple but general commit interface to support commit-reveal scheme. +author: Zainan Victor Zhou (@xinbenlv), Matt Stam (@mattstam) +discussions-to: https://ethereum-magicians.org/t/erc-5732-simple-commit-interface-to-support-commit-reveal-schemes/11115 +status: Last Call +last-call-deadline: 2022-11-10 +type: Standards Track +category: ERC +created: 2022-09-29 +requires: 165 +--- + +## Abstract + +A simple commit interface to support commit-reveal scheme which provides **only** a commit +method but no reveal method, allowing implementations to integrate this interface +with arbitrary reveal method such as `vote` or `transfer`. + +## Motivation + +1. support commit-reveal privacy for applications such as voting. +2. make it harder for attackers for front-running, back-running or sandwich attack. + +## Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +1. Compliant contract need to implement the following interface + +```solidity +pragma solidity >=0.7.0 <0.9.0; + +/// The EIP-165 identifier of this interface is 0x4ba43d48 +interface IERC_COMMIT { + event Commit(bytes32 _commitment, bytes _extraData); + function commit(bytes32 _commitment, bytes calldata _extraData) payable external; +} +``` + +2. One or more methods of a compliant contract MAY be used for reveal. But there MUST be a way to supply an extra field of `secret_salt`, +so that committer can later include the `secret_salt` in the reveal TX that exposes the `secret_salt`. +Size and location of `secret_salt` is intentionally unspecified in this EIP to allow general flexibility for integration. + +3. Compliant contract MUST implement [EIP-165](./eip-165.md). + +## Rationale + +1. One design options is that we can attach a Commit Interface to any individual ERCs such as voting standards or token standards. We choose to have a simple and generalize commit interface so all ERCs can be extended to support commit-reveal without changing their basic method signatures. + +2. The key derived design decision we made is we will have a standardized `commit` method without a standardized `reveal` method, making room for customized reveal method or using +`commit` with existing standard. + +## Backwards Compatibility + +This EIP is backward compatible with all existing ERCs method signature that has extraData. New EIPs can be designed with an extra field of "salt" +to make it easier to support this EIP, but not required. + +## Reference Implementation + +### Commit with Token-Transfer as Reveal + +Example of a Simple Transfer Standard being integrated with this EIP: + +```solidity +interface ISimpleToken { + function transfer(address to, uint256 amount); +} + +contract SomeToken is ISimpleToken { + mapping(address => uint256, bytes calldata extraData) balance; + function transfer(address to, uint256 amount, bytes calldata extraData) { + required(balance[msg.sender] > amount); + balance[msg.sender] -= amount; + balance[to] += amount; + } +} +``` + +When integrating with this EIP, it becomes this + +```solidity +interface ISimpleToken { + function transfer(address to, uint256 amount, bytes calldata extraData); +} + +contract SomeBetterToken is ISimpleToken, IERC_COMMIT { + mapping(address => uint256) balance; + mapping(address => bytes32) lastCommits; + function commit(bytes32 _commitment, bytes calldata _extraData) { + lastCommits[msg.sender] = _commitment; + emit Commit(...); + } + function transfer(address _to, uint256 _amount, bytes calldata _extraData/*first 32bytes are used as secret_sault*/) { + required(balance[msg.sender] > amount); + // pseudo code. + require(lastCommits[msg.sender] == _recomputeCommit(msg.sender, _to, _amount, _extraData[:32])); // the commitment from last sender was + delete lastCommits[msg.sender]; // immediately delete commits to avoid reentry attack. + balance[msg.sender] -= amount; + balance[to] += amount; + } + + function _recomputeCommit(address _sender, address _to, uint256 _amount, bytes32 _secretSalt) returns (bytes32){ + return keccak256(abi.encodePack(_sender, _to, _amount, _secretSalt)); + } + +} +``` + +### Commit with Voting as Reveal + +Example of a Simple Transfer Standard being integrated with this EIP: + +```solidity +interface ISimpleToken { + function vote(address _proposalId, uint8 _optionId, bytes calldata _extraData); +} + +contract FooVote is ISimpleToken { + mapping(address => uint256/*proposalId*/ => uint8/*optionId*/) ballots; + function vote(address _proposalId, uint8 _optionId, bytes calldata _extraData) { + ballots[msg.sender][_proposalId] = _optionId; + } + + // Ballot tally method omitted. +} +``` + +When integrating with this EIP, it becomes this + +```solidity +interface ISimpleVote { + function vote(uint256 _proposalId); +} + +contract BarVote is ISimpleVote, IERC_COMMIT { + mapping(address => address => bool) proposalVotes; + mapping(address => bytes32) lastCommits; + mapping(uint256 => uint256) proposalDeadlines; // block number of deadline for each proposal + + function commit(bytes32 _commitment, bytes calldata _extraData) { + lastCommits[msg.sender] = _commitment; + emit Commit(...); + } + + function vote(address _proposalId, uint8 _optionId, bytes calldata _extraData) { + // pseudo code. + require(lastCommits[msg.sender] == _recomputeCommit(msg.sender, _proposalId, _optionId, _extraData[:32])); // the commitment from last sender was + delete lastCommits[msg.sender]; // immediately delete commits to avoid reentry attack. + ballots[msg.sender][_proposalId] = _optionId; + } + + function _recomputeCommit(address _sender, address _proposalId, uint8 _optionId, bytes32 _secretSalt) returns (bytes32){ + return keccak256(abi.encodePack(_sender, _proposalId, _optionId, _secretSalt)); + } + + // Ballot tally method omitted. + // Proposal deadline method omitted. +} +``` + +### Commit with ENS Register as Reveal + +In ENS registering process, currently +inside of `ETHRegistrarController` contract +a commit function is being used to +allow registerer fairly register a desire +domain to avoid being front-run. + +Here is how ENS uses commitment in its registration logic: + +```solidity +function commit(bytes32 commitment) public { + require(commitments[commitment] + maxCommitmentAge < now); + commitments[commitment] = now; +} +``` + +With this EIP it can be updated to + +```solidity +function commit(bytes32 commitment, bytes calldata data) public { + require(commitments[commitment] + maxCommitmentAge < now); + commitments[commitment] = now; + emit Commit(...); +} +``` + +## Security Considerations + +1. Do not use the reference implementation in production. It is just for demonstration purposes. +2. The reveal transactions, parameters, especially `secret_salt` MUST be kept in secrecy before revealing to achieve privacy. +3. The length of `secret_salt` cryptographically long enough and the way to generate a `secret_salt` shall be cryptographically safe. +4. User shall NEVER reuse a used `secret_salt`. It's RECOMMENDED for client application to warn User for reusing a secret_salt. +5. Contract implementations SHOULD consider deleting the commitment of a given sender immediately to reduce chance of replay attack or re-entry attack by adversaries. +6. Contract implementations MAY consider also include the ordering of commitment received to add restriction on the order of reveal TX transactions. +7. Cautious on the potential replay attack across different chain-ids or chains resulting from forks, in which case, a ChainId shall be included in the generation of commitment. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5749.md b/EIPS/eip-5749.md new file mode 100644 index 00000000000000..1fcff16d33124a --- /dev/null +++ b/EIPS/eip-5749.md @@ -0,0 +1,119 @@ +--- +eip: 5749 +title: The 'window.evmproviders' object +description: Add 'window.evmproviders' and suggest the eventual removal of 'window.ethereum' +author: Kosala Hemachandra (@kvhnuke), Brett Kolodny (@brettkolodny) +discussions-to: https://ethereum-magicians.org/t/eip-5749-deprecate-window-ethereum/11195 +status: Draft +type: Standards Track +category: Interface +created: 2022-10-04 +requires: 1193 +--- + + +## Abstract +A Javascript Ethereum Provider interface injection that will allow for the interoperability of multiple browser wallets at the same time. Replacing `window.ethereum` with `window.evmproviders` is a simple solution that will provide multiple benefits including: improving user experience, encouraging innovation in the space, removing race conditions and a 'winner-takes-most' environment, and lowering barriers to user adoption. + +## Motivation +At present, `window.ethereum` is the prevailing method by which Ethereum-compatible applications interact with injected wallets. This originated with Mist Wallet in 2015 to interact with other applications. With the proliferation of both applications and wallets, `window.ethereum` has unintended negative consequences: + +- `window.ethereum` only permits one wallet to be injected at a time, resulting in a race condition between two or more wallets. This creates an inconsistent connection behavior that makes having and using more than one browser wallet unpredictable and impractical. The current solution is for wallets to inject their own namespaces, but this is not feasible as every application would need to be made aware of any wallet that might be used. +- The aforementioned race condition means users are disincentivized to experiment with new wallets. This creates a 'winner-takes-most' wallet market across EVM chains which forces application developers to optimize for a particular wallet experience. +- The 'winner-takes-most' wallet environment that results from the `window.ethereum` standard hinders innovation because it creates a barrier to adoption. New entrants into the space have difficulty gaining traction against legacy players because users can have no more than one injected wallet. With new entrants crowded out, legacy wallet providers are put under little pressure to innovate. +- Wallets continue to be the most fundamental tool for interacting with blockchains. A homogeneous wallet experience in Ethereum and EVM chains risks stunting UX improvement across the ecosystem and will allow other ecosystems that are more encouraging of competition and innovation to move ahead. +- Some wallets that currently use `window.ethereum` as of August, 2022. Currently a user will have inconsistent behavior if they use multiple of these wallets in a single browser. + - Metamask + - Coinbase wallet + - Enkrypt + - Trust wallet + - Rainbow ..etc + +Replacing `window.ethereum` with `window.evmproviders` will allow solutions such as web3modal and web3onboard to only display all injected wallets the user has installed. This will simpify the UX and remove race conditions between wallet providers in case multiple wallets are installed. Over time, as `window.evmproviders` supplants the current standard and removes barriers to choice, we can hope to see a wallet landscape more reflective of user preference. + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +### `window.evmproviders={}` + +```javascript +/** + * @typedef {Object} ProviderInfo + * @property {string} uuid + * @property {string} name + * @property {string} icon - format: `data:image/svg+xml;base64,${string}` + * @property {number} description + */ + +/** + * @typedef {EIP1193Provider} ProviderWithInfo + * @property {ProviderInfo} info + */ + +/** + * @typedef {Object.} EVMProvidersType + */ + +/** + * @typedef {Object} Window + * @property {EVMProvidersType} evmproviders + */ + +``` + +Type `EIP1193Provider` is well documented at [EIP-1193](./eip-1193.md) + +``` +interface ProviderInfo +uuid: UUIDv4 unique to the provider +name: Name of the Wallet +icon: base64 encoded svg image +description: Description for your wallet +``` + +``` +interface EVMProvidersType +key is RECOMMENDED to be the name of the extension +``` + +By adopting an object for EIP-1193 compliant providers we can have multiple different ethereum/evm compatible wallets coexists in the same browser. This will prevent race conditions and inconsistent behaviors. + +## Rationale +By introducing `ProviderInfo` type web onboarding libraries such as + Web3Modal + Web3React + Web3Onboard +can easily grab the necessary information to populate their popup window to choose the wallet. + +The name `evmproviders` was chosen in order to be inclusive of other evm-compliant chains. + +**data:image/svg+xml;** svg data uri was chosen since it is easier to be modified if the application requires for example different size for the image. + +## Backwards Compatibility +This EIP doesn't require supplanting `window.ethereum`, so it doesn't directly break existing applications. However, the recommended behavior of eventually supplanting `window.ethereum` would break existing applications that rely on it. + + +## Reference Implementation + +### Injection + +```typescript +const provider: ProviderWithInfo = [your wallet] +window.evmproviders = window.evmproviders || {}; +window.evmproviders[name] = provider +``` + +### Retrieving all EVM providers + +```typescript +const allproviders = Object.values(window.evmproviders) +``` + +## Security Considerations + +The security considerations of EIP-1193 apply for this EIP. + +The use of SVG images introduces a cross-site scripting risk as they can include JavaScript code. Applications and libraries must render SVG images using the `` tag to make sure no JS executions can happen. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5750.md b/EIPS/eip-5750.md new file mode 100644 index 00000000000000..aeb6a7513607ad --- /dev/null +++ b/EIPS/eip-5750.md @@ -0,0 +1,88 @@ +--- +eip: 5750 +title: Extra Data Parameter in Methods +description: Defines an extra data parameter in methods. +author: Zainan Victor Zhou (@xinbenlv) +discussions-to: https://ethereum-magicians.org/t/erc-5750-method-with-extra-data/11176 +status: Draft +type: Standards Track +category: ERC +created: 2022-10-04 +--- + +## Abstract + +This EIP defines an extra data parameter in methods, denoted as `methodName(... bytes calldata _data)`. Compliant methods +can use the extra data to introduce extended behaviors. Any method, regardless of name, can use this extra parameter. + +## Motivation + +The general purpose of having extra data in a method is to allow further extensions for a existing method interface. + +1. Any methods complying with this EIP, such as overloaded `transfer` and `vote` could use string reasons as the extra data. +2. Existing EIPs that have exported methods compliant with this EIP can be extended for behaviors such as using the extra data to prove endorsement or as a salt, nonce, or commitments for reveal/commit schemes. +3. Data can be passed forward to callbacks. + +## Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +1. Any compliant contract's compliant method MUST have a `bytes` (dynamic size) as its last parameter of the method. + +```solidity +function methodName(type1 param1, type2, param2 ... bytes calldata data); +``` + +## Rationale + +1. Having using the dynamically-sized `bytes` type allow for maximum flexibility to enable additional payloads of arbitrary types +2. Having the bytes specified as the last parameter makes this EIP compatible with the calldata layout of solidity. + +## Backwards Compatibility + +Many of existing EIPs already have compliant methods and all contracts compliant with those EIPs are already compliant with this EIP. + +Here is an incomplete list: + +1. In [EIP-721](./eip-721.md) the following methods are already compliant: + +- `function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;` is already compliant + +2. In [EIP-1155](./eip-1155.md) the following methods are already compliant + +- `function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) external;` +- `function safeBatchTransferFrom(address _from, address _to, uint256[] calldata _ids, uint256[] calldata _values, bytes calldata _data) external;` + +3. In [EIP-777](./eip-777.md) the following methods are already compliant + +- `function burn(uint256 amount, bytes calldata data) external;` +- `function send(address to, uint256 amount, bytes calldata data) external;` + +4. In [EIP-2535](./eip-2535.md) the following methods are already compliant + +```solidity +function diamondCut( + FacetCut[] calldata _diamondCut, + address _init, + bytes calldata _calldata + ) external; +``` + +5. In [EIP-1271](./eip-1271.md) the following methods are already compliant: + +```solidity + function isValidSignature( + bytes32 _hash, + bytes memory _signature) + public + view + returns (bytes4 magicValue); +``` + +## Security Considerations + +1. If using the extra data for extended behavior, such as supplying signature for onchain verification, or supplying commitments in a commit-reveal scheme, best practices should be followed for those particular extended behaviors. +2. Compliant contracts must also take into consideration that the data parameter will be publicly revealed when submitted into the mempool or included in a block, so one must consider the risk of replay, and transaction ordering attacks. In addition, unencrypted personally identifiable information should never be included in the data parameter. + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5791.md b/EIPS/eip-5791.md new file mode 100644 index 00000000000000..0b1048104cbe94 --- /dev/null +++ b/EIPS/eip-5791.md @@ -0,0 +1,165 @@ +--- +eip: 5791 +title: Physical Backed Tokens +description: Minimal interface for linking ownership of EIP-721 NFTs to a physical chip +author: 2pmflow (@2pmflow), locationtba (@locationtba), Cameron Robertson (@ccamrobertson), cygaar (@cygaar) +discussions-to: https://ethereum-magicians.org/t/physical-backed-tokens/11350 +status: Draft +type: Standards Track +category: ERC +created: 2022-10-17 +requires: 191, 721 +--- + +## Abstract + +This standard is an extension of [EIP-721](./eip-721.md). It proposes a minimal interface for a [EIP-721](./eip-721.md) NFT to be "physically backed" and owned by whoever owns the NFT's physical counterpart. + +## Motivation + +NFT collectors enjoy collecting digital assets and sharing them with others online. However, there is currently no such standard for showcasing physical assets as NFTs with verified authenticity and ownership. Existing solutions are fragmented and tend to be susceptible to at least one of the following: + +- The ownership of the physical item and the ownership of the NFT are decoupled. + +- Verifying the authenticity of the physical item requires action from a trusted 3rd party (e.g. StockX). + +## Specification + +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +### Requirements + +This approach requires that the physical item must have a chip attached to it that fulfills the following requirements: + +- The chip can securely generate and store an ECDSA secp256k1 asymmetric key pair; +- The chip can sign messages using the private key of the previously-generated asymmetric key pair; +- The chip exposes the public key; and +- The private key cannot be extracted + +The approach also requires that the contract uses an account-bound implementation of [EIP-721](./eip-721.md) (where all [EIP-721](./eip-721.md) functions that transfer must throw, e.g. the "read only NFT registry" implementation referenced in [EIP-721](./eip-721.md)). This ensures that ownership of the physical item is required to initiate transfers and manage ownership of the NFT, through a new function introduced in this interface described below. + +### Approach + +Each NFT is conceptually linked to a physical chip. + +When the NFT is minted, it must also emit an event that includes the corresponding chip address (20-byte address derived from the chip's public key). This lets downstream indexers know which chip addresses are mapped to which tokens for the NFT collection. The NFT cannot be minted without its token id being linked to a specific chip. + +The interface includes a function called `transferTokenWithChip` that transfers the NFT to the function caller if a valid signature signed by the chip is passed in. A valid signature must follow the schemes set forth in [EIP-191](./eip-191.md) and [EIP-2](./eip-2.md) (s-value restrictions), where the data to sign consists of the target recipient address (the function caller) and a recent blockhash (the level of recency is up to the implementation). + +The interface also includes other functions that let anyone validate whether the chip in the physical item is backing an existing NFT in the collection. + +### Interface + +```solidity + +interface IERC5791 { + /// @notice Returns the token id for a given chip address. + /// @dev Throws if there is no existing token for the chip in the collection. + /// @param chipAddress The address for the chip embedded in the physical item (computed from the chip's public key). + /// @return The token id for the passed in chip address. + function tokenIdFor(address chipAddress) external view returns (uint256); + + /// @notice Returns true if the chip for the specified token id is the signer of the signature of the payload. + /// @dev Throws if tokenId does not exist in the collection. + /// @param tokenId The token id. + /// @param payload Arbitrary data that is signed by the chip to produce the signature param. + /// @param signature Chip's signature of the passed-in payload. + /// @return Whether the signature of the payload was signed by the chip linked to the token id. + function isChipSignatureForToken(uint256 tokenId, bytes calldata payload, bytes calldata signature) + external + view + returns (bool); + + /// @notice Transfers the token into the message sender's wallet. + /// @param signatureFromChip An EIP-191 signature of (msgSender, blockhash), where blockhash is the block hash for blockNumberUsedInSig. + /// @param blockNumberUsedInSig The block number linked to the blockhash signed in signatureFromChip. Should be a recent block number. + /// @param useSafeTransferFrom Whether EIP-721's safeTransferFrom should be used in the implementation, instead of transferFrom. + /// + /// @dev The implementation should check that block number be reasonably recent to avoid replay attacks of stale signatures. + /// The implementation should also verify that the address signed in the signature matches msgSender. + /// If the address recovered from the signature matches a chip address that's bound to an existing token, the token should be transferred to msgSender. + /// If there is no existing token linked to the chip, the function should error. + function transferTokenWithChip( + bytes calldata signatureFromChip, + uint256 blockNumberUsedInSig, + bool useSafeTransferFrom + ) external; + + /// @notice Calls transferTokenWithChip as defined above, with useSafeTransferFrom set to false. + function transferTokenWithChip(bytes calldata signatureFromChip, uint256 blockNumberUsedInSig) external; + + /// @notice Emitted when a token is minted + event PBTMint(uint256 indexed tokenId, address indexed chipAddress); + + /// @notice Emitted when a token is mapped to a different chip. + /// Chip replacements may be useful in certain scenarios (e.g. chip defect). + event PBTChipRemapping(uint256 indexed tokenId, address indexed oldChipAddress, address indexed newChipAddress); +} + +``` + +To aid recognition that an [EIP-721](./eip-721.md) token implements physical binding via this EIP: upon calling [EIP-165](./eip-165.md)’s `function supportsInterface(bytes4 interfaceID) external view returns (bool)` with `interfaceID=0x4901df9f`, a contract implementing this EIP must return true. + +The mint interface is up to the implementation. The minted NFT's owner should be the owner of the physical chip (this authentication could be implemented using the signature scheme defined for `transferTokenWithChip`). + +## Rationale + +This solution's intent is to be the simplest possible path towards linking physical items to digital NFTs without a centralized authority. + +The interface includes a `transferTokenWithChip` function that's opinionated with respect to the signature scheme, in order to enable a downstream aggregator-like product that supports transfers of any NFTs that implement this EIP in the future. + +### Out of Scope + +The following are some peripheral problems that are intentionally not within the scope of this EIP: + +- trusting that a specific NFT collection's chip addresses actually map to physical chips embedded in items, instead of arbitrary EOAs +- ensuring that the chip does not deterioriate or get damaged +- ensuring that the chip stays attached to the physical item +- etc. + +Work is being done on these challenges in parallel. + +Mapping token ids to chip addresses is also out of scope. This can be done in multiple ways, e.g. by having the contract owner preset this mapping pre-mint, or by having a `(tokenId, chipAddress)` tuple passed into a mint function that's pre-signed by an address trusted by the contract, or by doing a lookup in a trusted registry, or by assigning token ids at mint time first come first served, etc. + +Additionally, it's possible for the owner of the physical item to transfer the NFT to a wallet owned by somebody else (by sending a chip signature to that other person for use). We still consider the NFT physical backed, as ownership management is tied to the physical item. This can be interpreted as the item's owner temporarily lending the item to somebody else, since (1) the item's owner must be involved for this to happen as the one signing with the chip, and (2) the item's owner can reclaim ownership of the NFT at any time. + +## Backwards Compatibility + +This proposal is backward compatible with [EIP-721](./eip-721.md) on an API level. As mentioned above, for the token to be physical-backed, the contract must use a account-bound implementation of [EIP-721](./eip-721.md) (all [EIP-721](./eip-721.md) functions that transfer must throw) so that transfers go through the new function introduced here, which requires a chip signature. + +## Reference Implementation + +The following is a snippet on how to recover a chip address from a signature. + +```solidity +import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; + +function getChipAddressFromChipSignature( + bytes calldata signatureFromChip, + uint256 blockNumberUsedInSig +) internal returns (TokenData memory) { + if (block.number <= blockNumberUsedInSig) { + revert InvalidBlockNumber(); + } + unchecked { + if (block.number - blockNumberUsedInSig > getMaxBlockhashValidWindow()) { + revert BlockNumberTooOld(); + } + } + bytes32 blockHash = blockhash(blockNumberUsedInSig); + bytes32 signedHash = keccak256(abi.encodePacked(_msgSender(), blockHash)) + .toEthSignedMessageHash(); + address chipAddr = signedHash.recover(signatureFromChip); +} + +``` + +## Security Considerations + +The [EIP-191](./eip-191.md) signature passed to `transferTokenWithChip` requires the function caller's address in its signed data so that the signature cannot be used in a replay attack. It also requires a recent blockhash so that a malicious chip owner cannot pre-generate signatures to use after a short time window (e.g. after the owner of the physical item changes). + +Additionally, the level of trust that one has for whether the token is physically-backed is dependent on the security of the physical chip, which is out of scope for this EIP as mentioned above. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/EIPS/eip-5806.md b/EIPS/eip-5806.md new file mode 100644 index 00000000000000..4f2fd5133bbd4e --- /dev/null +++ b/EIPS/eip-5806.md @@ -0,0 +1,70 @@ +--- +eip: 5806 +title: Delegate transaction +description: Adds a new transaction type that allows a EOAs to execute arbitrary code through delegation +author: Hadrien Croubois (@Amxx) +discussions-to: https://ethereum-magicians.org/t/eip-5806-delegate-transaction/11409 +status: Draft +type: Standards Track +category: Core +created: 2022-10-20 +requires: 2718, 2930 +--- + +## Abstract + +This EIP adds a new transaction type that allows EOAs to execute arbitrary code using a delegate-call-like mechanism. + +## Motivation + +Account abstraction has been extensively discussed but the path toward mainstream adoption is still unclear. Some approaches, such as [EIP-4337](./eip-4337.md) hope to improve the usability of smart wallets, without addressing the issue of smart wallet support by applications. [EIP-3074](./eip-3074.md) proposes another approach that favors existing EOAs but comes with replay risks. + +This EIP proposes a simpler approach that addresses some of the objectives of account abstraction for EOAs with minimal change over the EVM. By allowing EOAs to perform delegate calls to a contract (similarly to how contracts can delegate calls to other contracts using [EIP-7](./eip-7.md)), EOAs will be able to have more control over what operations they want to execute. + +Performing a delegate call to a multicall contract (such as the one deployed to `0xcA11bde05977b3631167028862bE2a173976CA11`), EOAs would be able to batch multiple transactions into a single one, while being the `msg.sender` of all the sub calls. Other unforeseen logic could be implemented in smart contracts and used by EOA. This includes emitting events, using storage under the EOA's account, or even deploying contracts using `create2`. + +This EIP doesn't aim to replace other account abstraction proposals. It hopes to be an easy-to-implement alternative that would significantly improve the user experience of EOA owners in the near future. + +## Specification +The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. + +### Parameters +- `FORK_BLKNUM` = `TBD` +- `TX_TYPE` = TBD, > 0x02 ([EIP-1559](./eip-1559.md)) + +As of `FORK_BLOCK_NUMBER`, a new [EIP-2718](./eip-2718.md) transaction is introduced with `TransactionType` = `TX_TYPE(TBD)`. + +The intrinsic cost of the new transaction is inherited from [EIP-2930](./eip-2930.md), specifically `21000 + 16 * non-zero calldata bytes + 4 * zero calldata bytes + 1900 * access list storage key count + 2400 * access list address count`. + +The [EIP-2718](./eip-2718.md) `TransactionPayload` for this transaction is + +``` +rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, data, access_list, signature_y_parity, signature_r, signature_s]) +``` + +The definitions of all fields share the same meaning with [EIP-1559](./eip-1559.md). Note the absence of `amount` field in this transaction! + +The `signature_y_parity, signature_r, signature_s` elements of this transaction represent a secp256k1 signature over `keccak256(0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, data, access_list]))`. + +The [EIP-2718](./eip-2718.md) `ReceiptPayload` for this transaction is `rlp([status, cumulative_transaction_gas_used, logs_bloom, logs])`. + +The execution of this new transaction type is equivalent to the delegate call mechanism introduced in [EIP-7](./eip-7.md), but performed by an EOA (the transaction sender). This implies that the code present at `destination`, if any, should be executed in the context of the sender. As a consequence, such a transaction can set and read storage under the EOA. It can also emit an event from the EOA. + +## Rationale + +EOAs are the most widely used type of wallet. + +This EIP would drastically expand the ability of EOAs to interact with smart contracts by using the pre-existing and well-understood delegation mechanism introduced in [EIP-7](./eip-7.md) and without adding new complexity to the EVM. + +## Backwards Compatibility + +No known backward compatibility issues thanks to the transaction envelope ([EIP-2718](./eip-2718.md)) + +## Security Considerations + +The nonce mechanism, already used in other transaction types, prevents replay attacks. This makes this approach safer, but also less powerful than [EIP-3074](./eip-3074.md). + +Contracts being called through this mechanism can execute any operation on behalf of the signer. Signers should be extremely careful signing this transaction (just like any other transaction). + +## Copyright +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/README.md b/README.md index b18097c8069fc4..d3b1ee0834e842 100644 --- a/README.md +++ b/README.md @@ -13,28 +13,29 @@ If you would like to become an EIP Editor, please check [EIP-5069](./EIPS/eip-50 ## Mission -The goal of the EIP project is to document standardized protocols for Ethereum clients and applications and to document them in a high quality and implementable way. +The goal of the EIP project is to document standardized protocols for Ethereum clients and applications and to document them in a high-quality and implementable way. ## Preferred Citation Format -The canonical URL for a EIP that has achieved draft status at any point is at https://eips.ethereum.org/. For example, the canonical URL for EIP-1 is https://eips.ethereum.org/EIPS/eip-1. +The canonical URL for an EIP that has achieved draft status at any point is at . For example, the canonical URL for EIP-1 is . -Please consider anything which is not published on https://eips.ethereum.org/ as a working paper. +Please consider anything which is not published on as a working paper. -And please consider anything published at https://eips.ethereum.org/ with a status of "draft" as an incomplete draft. +And please consider anything published at with a status of "draft" as an incomplete draft. ## Automerger -This repository contains an "auto merge" feature to ease the workload for EIP editors. Pull requests to any EIP will be auto-merged if the EIP's authors approve the PR on GitHub. This is handled by the [EIP-Bot](https://github.com/ethereum/EIP-Bot). +This repository contains an "auto-merge" feature to ease the workload for EIP editors. Pull requests to any EIP will be auto-merged if the EIP's authors approve the PR on GitHub. This is handled by [EIP-Bot](https://github.com/ethereum/EIP-Bot). ## Validation Pull requests in this repository must pass automated validation checks: -* HTML formatting and broken links are [checked](https://github.com/ethereum/EIPs/blob/master/.travis-ci.sh) using [html-proofer](https://rubygems.org/gems/html-proofer). -* EIP front matter and formatting are [checked](https://github.com/ethereum/EIPs/blob/master/.github/workflows/auto-merge-bot.yml) using [EIP Validator](https://github.com/ethereum/eipv). +- HTML formatting, broken links, and EIP front matter/formatting are [checked](https://github.com/ethereum/EIPs/blob/master/.github/workflows/ci.yml) using [html-proofer](https://rubygems.org/gems/html-proofer) and [`eipw`](https://github.com/ethereum/eipw). +- Required pull request reviews are [enforced](https://github.com/ethereum/EIPs/blob/master/.github/workflows/auto-review-bot.yml) using [EIP-Bot](https://github.com/ethereum/EIP-Bot/). It is possible to run the EIP validator locally: + ```sh cargo install eipv eipv @@ -74,6 +75,6 @@ eipv bundle exec jekyll serve ``` -2. Preview your local Jekyll site in your web browser at http://localhost:4000. +2. Preview your local Jekyll site in your web browser at . -More information on Jekyll and GitHub pages [here](https://help.github.com/en/enterprise/2.14/user/articles/setting-up-your-github-pages-site-locally-with-jekyll). +More information on Jekyll and GitHub Pages [here](https://help.github.com/en/enterprise/2.14/user/articles/setting-up-your-github-pages-site-locally-with-jekyll). diff --git a/_layouts/eip.html b/_layouts/eip.html index 8da918d2c4ea28..198e6a13694587 100644 --- a/_layouts/eip.html +++ b/_layouts/eip.html @@ -92,3 +92,11 @@

Citation

"copyrightYear": "{{ page.created | date: "%Y" }}" } + diff --git a/assets/eip-2535/Contributors.md b/assets/eip-2535/Contributors.md new file mode 100644 index 00000000000000..de9097ea80fd7a --- /dev/null +++ b/assets/eip-2535/Contributors.md @@ -0,0 +1,79 @@ +## Contributors + +* Andrew Redden (@androolloyd) +* Patrick Gallagher (@pi0neerpat) +* Leo Alt (@leonardoalt) +* Santiago Palladino (@spalladino) +* William Entriken (@fulldecent) +* Gonçalo Sá (@GNSPS) +* Brian Burns (@Droopy78) +* Ramesh Nair(@hiddentao) +* Jules Goddard (@JulesGoddard) +* Micah Zoltu (@MicahZoltu) +* Sam Wilson (@SamWilsn) +* William Morriss (@wjmelements) +* Zachary (@Remscar) +* Patrick Collins (@PatrickAlphaC) +* Hadrien Croubois (@Amxx) +* (@farreldarian) +* Kelvin Schoofs (@SchoofsKelvin) +* (@0xpApaSmURf) +* Nathan Sala (@nataouze) +* Anders Torbjornsen (@anders-torbjornsen) +* (@Pandapip1) +* Xavier Iturralde (@xibot) +* Coder Dan (@cinnabarhorse) +* GldnXross (@gldnxross) +* Christian Reitwiessner (@chriseth) +* Timidan (@Timidan) +* cyotee doge (@cyotee) +* Glory Praise Emmanuel (@emmaglorypraise) +* Ed Zynda (@ezynda3) +* Arthur Nesbitt (@nesbitta) +* Cliff Hall (@cliffhall) +* Tyler Scott Ward (@tylerscottward) +* Troy Murray (@DannyDesert) +* Dan Finlay (@danfinlay) +* Theodore Georgas (@tgeorgas) +* Aditya Palepu (@apalepu23) +* Ronan Sandford (@wighawag) +* Markus Waas (@gorgos) +* Blessing Emah (@BlessingEmah) +* Andrew Edwards +* Ashwin Yardi (@ashwinYardi) +* Marco Castignoli (@marcocastignoli) +* Blaine Bublitz (@phated) +* Bearded +* Nick Barry (@ItsNickBarry) +* (@Vectorized) +* Rachit Srivastava (@rachit2501) +* Neeraj Kashyap (@zomglings) +* Zac Denham (@zdenham) +* JA (@ubinatus) +* Carter Carlson (@cartercarlson) +* James Sayer (@jamessayer98) +* Arpit Temani (@temaniarpit27) +* Parv Garg (@parv3213) +* Publius (@publiuss) +* Guy Hance (@guyhance) +* Payn (@Ayuilos) +* Luis Schliesske (@gitpusha) +* Hilmar Orth (@hilmarx) +* Matthieu Marie Joseph (@Gauddel) +* David Uzochukwu (@davidpius95) +* TJ VanSlooten (@tjvsx) +* 0xFluffyBeard (@0xFluffyBeard) +* Florian Pfeiffer (@FlorianPfeifferKanaloaNetwork) +* Mick de Graaf(@MickdeGraaf) +* Alessio Delmonti (@Alexintosh) +* Neirenoir (@Neirenoir) +* Evert Kors (@Evert0x) +* Patrick Kim (@pakim249CAL) +* Ersan YAKIT (@ersanyakit) +* Matias Arazi (@MatiArazi) +* Lucas Grasso Ramos (@LucasGrasso) +* Nikolay Angelov (@NikolayAngelov) +* John Reynolds (@gweiworld) +* Viraz Malhotra (@viraj124) +* Kemal Emre Ballı (@emrbli) +* Zack Peng (@zackpeng) diff --git a/assets/eip-2535/reference/EIP2535-Diamonds-Reference-Implementation.zip b/assets/eip-2535/reference/EIP2535-Diamonds-Reference-Implementation.zip index 08a4eb1a74e311..9be228ed4d37c2 100644 Binary files a/assets/eip-2535/reference/EIP2535-Diamonds-Reference-Implementation.zip and b/assets/eip-2535/reference/EIP2535-Diamonds-Reference-Implementation.zip differ diff --git a/assets/eip-3475/ERC3475.sol b/assets/eip-3475/ERC3475.sol index 3cfeb3c0253c1e..157476587c42f1 100644 --- a/assets/eip-3475/ERC3475.sol +++ b/assets/eip-3475/ERC3475.sol @@ -7,15 +7,17 @@ import "./interfaces/IERC3475.sol"; contract ERC3475 is IERC3475 { /** * @notice this Struct is representing the Nonce properties as an object + * */ struct Nonce { - mapping(uint256 => IERC3475.Values) _values; - // stores the values corresponding to the dates (issuance and maturity date). + mapping(uint256 => IERC3475.Values) _values; + // storing the issuance of the issued bonds with their balances. mapping(address => uint256) _balances; + // defines the mapping for amount of bonds that can be delegated by Owner => operator address. mapping(address => mapping(address => uint256)) _allowances; - // supplies of this nonce + // Overall supplies of this nonce uint256 _activeSupply; uint256 _burnedSupply; uint256 _redeemedSupply; @@ -23,7 +25,7 @@ contract ERC3475 is IERC3475 { /** * @notice this Struct is representing the Class properties as an object - * and can be retrieved by the classId + * and can be retrieved by the classId */ struct Class { mapping(uint256 => IERC3475.Values) _values; @@ -43,12 +45,11 @@ contract ERC3475 is IERC3475 { * to be deployed during the initial deployment cycle */ constructor() { - // define "symbol of the class"; + // define "symbol of the given class"; _classMetadata[0].title = "symbol"; _classMetadata[0]._type = "string"; _classMetadata[0].description = "symbol of the class"; _classes[0]._values[0].stringValue = "DBIT Fix 6M"; - _classMetadata[1].title = "symbol"; _classMetadata[1]._type = "string"; _classMetadata[1].description = "symbol of the class"; @@ -59,7 +60,7 @@ contract ERC3475 is IERC3475 { _classMetadata[5]._type = "int"; _classMetadata[5].description = "details about issuance and redemption time"; - // define the maturity time period (for the test class). + // define the maturity time period (for the test class). _classes[0]._values[5].uintValue = 10; _classes[1]._values[5].uintValue = 1; @@ -73,7 +74,7 @@ contract ERC3475 is IERC3475 { _classes[1].nonces[1]._values[0].uintValue = block.timestamp + 2; _classes[1].nonces[2]._values[0].uintValue = block.timestamp + 3; - // define "maturity of the nonce"; + // define "maturity of the nonce"; _classes[0]._nonceMetadata[0].title = "maturity"; _classes[0]._nonceMetadata[0]._type = "int"; _classes[0]._nonceMetadata[0].description = "maturity date in integer"; @@ -87,7 +88,7 @@ contract ERC3475 is IERC3475 { _classes[0].nonces[2]._values[0].boolValue = true; } - // WRITABLES + // Writable functions. function transferFrom( address _from, address _to, @@ -294,7 +295,6 @@ contract ERC3475 is IERC3475 { return (_classes[classId]._values[metadataId]); } - function nonceValues(uint256 classId, uint256 nonceId, uint256 metadataId) external view @@ -303,7 +303,7 @@ contract ERC3475 is IERC3475 { return (_classes[classId].nonces[nonceId]._values[metadataId]); } - /** determines the progress till the redemption of the bonds is valid (based on the type of bonds class). + /** determines the progress till the redemption of the bonds is valid (based on the type of bonds class). * @notice ProgressAchieved and `progressRemaining` is abstract. For e.g. we are giving time passed and time remaining. */ @@ -311,7 +311,7 @@ contract ERC3475 is IERC3475 { public view override - returns (uint256 progressAchieved, uint256 progressRemaining){ + returns (uint256 progressAchieved, uint256 progressRemaining){ uint256 issuanceDate = _classes[classId].nonces[nonceId]._values[0].uintValue; uint256 maturityDate = issuanceDate + _classes[classId].nonces[nonceId]._values[5].uintValue; @@ -334,7 +334,7 @@ contract ERC3475 is IERC3475 { } /** - checks the status of approval to transfer the ownership of bonds by _owner to operator. + checks the status of approval to transfer the ownership of bonds by _owner to operator. */ function isApprovedFor( address _owner, @@ -355,7 +355,7 @@ contract ERC3475 is IERC3475 { "ERC3475: not enough bond to transfer" ); - //transfer balance + //transfer balance nonce._balances[_from] -= _transaction._amount; nonce._balances[_to] += _transaction._amount; } @@ -383,7 +383,7 @@ contract ERC3475 is IERC3475 { address _to, IERC3475.Transaction calldata _transaction ) private { - Nonce storage nonce = _classes[_transaction.classId].nonces[_transaction.nonceId]; + Nonce storage nonce = _classes[_transaction.classId].nonces[_transaction.nonceId]; //transfer balance nonce._balances[_to] += _transaction._amount; @@ -396,7 +396,7 @@ contract ERC3475 is IERC3475 { IERC3475.Transaction calldata _transaction ) private { Nonce storage nonce = _classes[_transaction.classId].nonces[_transaction.nonceId]; - // verify whether _amount of bonds to be redeemed are sufficient available for the given nonce of the bonds + // verify whether _amount of bonds to be redeemed are sufficient available for the given nonce of the bonds require( nonce._balances[_from] >= _transaction._amount, diff --git a/assets/eip-3475/ERC3475.test.ts b/assets/eip-3475/ERC3475.test.ts index f5b9d6a7e9cd96..cf94602b1e2016 100644 --- a/assets/eip-3475/ERC3475.test.ts +++ b/assets/eip-3475/ERC3475.test.ts @@ -90,7 +90,7 @@ contract('Bond', async (accounts: string[]) => { it('lender should redeem bonds when conditions are met', async () => { const redemptionTransaction: _transaction[] = [ { - classId: 1, + classId: 1, nonceId: 1, amount: 2000 @@ -111,7 +111,7 @@ contract('Bond', async (accounts: string[]) => { const redemptionTransaction: _transaction[] = [ { - classId: 0, + classId: 0, nonceId: 0, amount: 2000 @@ -168,7 +168,7 @@ contract('Bond', async (accounts: string[]) => { }]; await bondContract.issue(lender, transactionRedeem, {from: lender}); - sleep(7000); + sleep(7000); const tx = (await bondContract.redeem(lender, transactionRedeem, {from:lender})).tx; @@ -185,7 +185,6 @@ contract('Bond', async (accounts: string[]) => { }]; await bondContract.issue(lender, transactionRedeem, {from: lender}); - const tx = (await bondContract.burn(lender, transactionRedeem, {from:lender})).tx; console.log(tx) assert.isString(tx); diff --git a/assets/eip-3475/interfaces/IERC3475.sol b/assets/eip-3475/interfaces/IERC3475.sol index a9ee19a6a26649..9dc5a45e06deb4 100644 --- a/assets/eip-3475/interfaces/IERC3475.sol +++ b/assets/eip-3475/interfaces/IERC3475.sol @@ -5,22 +5,21 @@ pragma solidity ^0.8.0; interface IERC3475 { - // STRUCTURE /** * @dev Values structure of the Metadata */ - struct Values { + struct Values { string stringValue; uint uintValue; address addressValue; bool boolValue; } /** - * @dev structure allows the transfer of any given number of bonds from one address to another. - * @title": "defining the title information", - * @type": "explaining the type of the title information added", - * @description": "little description about the information stored in the bond", + * @dev structure allows to define particular bond metadata (ie the values in the class as well as nonce inputs). + * @notice 'title' defining the title information, + * @notice '_type' explaining the data type of the title information added (eg int, bool, address), + * @notice 'description' explains little description about the information stored in the bond", */ struct Metadata { string title; @@ -28,7 +27,8 @@ interface IERC3475 { string description; } /** - * @dev structure allows the transfer of any given number of bonds from one address to another. + * @dev structure that defines the parameters for specific issuance of bonds and amount which are to be transferred/issued/given allowance, etc. + * @notice this structure is used to streamline the input parameters for functions of this standard with that of other Token standards like ERC20. * @classId is the class id of the bond. * @nonceId is the nonce id of the given bond class. This param is for distinctions of the issuing conditions of the bond. * @amount is the amount of the bond that will be transferred. @@ -42,7 +42,7 @@ interface IERC3475 { // WRITABLES /** * @dev allows the transfer of a bond from one address to another (either single or in batches). - * @param _from is the address of the holder whose balance is about to decrease. + * @param _from is the address of the holder whose balance is about to decrease. * @param _to is the address of the recipient whose balance is about to increase. * @param _transactions is the object defining {class,nonce and amount of the bonds to be transferred}. */ @@ -55,13 +55,12 @@ interface IERC3475 { */ function transferAllowanceFrom(address _from, address _to, Transaction[] calldata _transactions) external; /** - * @dev allows issuing of any number of bond types to an address. + * @dev allows issuing of any number of bond types to an address(either single/batched issuance). * The calling of this function needs to be restricted to bond issuer contract. * @param _to is the address to which the bond will be issued. * @param _transactions is the object defining {class,nonce and amount of the bonds to be issued for given whitelisted bond}. */ function issue(address _to, Transaction[] calldata _transactions) external; - /** * @dev allows redemption of any number of bond types from an address. * The calling of this function needs to be restricted to bond issuer contract. @@ -73,7 +72,7 @@ interface IERC3475 { /** * @dev allows the transfer of any number of bond types from an address to another. * The calling of this function needs to be restricted to bond issuer contract. - * @param _from is the address of the holder whose balance about to decrees. + * @param _from is the address of the holder whose balance about to decrees. * @param _transactions is the object defining {class,nonce and amount of the bonds to be redeemed for given whitelisted bond}. */ function burn(address _from, Transaction[] calldata _transactions) external; @@ -124,34 +123,39 @@ interface IERC3475 { /** * @dev Returns the JSON metadata of the classes. * The metadata SHOULD follow a set of structure explained later in eip-3475.md + * @param metadataId is the index corresponding to the class parameter that you want to return from mapping. */ function classMetadata(uint256 metadataId) external view returns ( Metadata memory); /** - * @dev Returns the JSON metadata of the nonces. - * The metadata SHOULD follow a set of structure explained later in eip-3475.md + * @dev Returns the JSON metadata of the Values of the nonces in the corresponding class. + * @param classId is the specific classId of which you want to find the metadata of the corresponding nonce. + * @param metadataId is the index corresponding to the class parameter that you want to return from mapping. + * @notice The metadata SHOULD follow a set of structure explained later in metadata section. */ function nonceMetadata(uint256 classId, uint256 metadataId) external view returns ( Metadata memory); /** * @dev Returns the values of the given classId. + * @param classId is the specific classId of which we want to return the parameter. + * @param metadataId is the index corresponding to the class parameter that you want to return from mapping. * the metadata SHOULD follow a set of structures explained in eip-3475.md */ function classValues(uint256 classId, uint256 metadataId) external view returns ( Values memory); /** * @dev Returns the values of given nonceId. - * @param metadataId index number of structure as explained in the metadata section in eip-3475.md + * @param metadataId index number of structure as explained in the metadata section in EIP-3475. * @param classId is the class of bonds for which you determine the nonce. * @param nonceId is the nonce for which you return the value struct info. - * Returns the values object corresponding to the given value. + * Returns the values object corresponding to the given value. */ function nonceValues(uint256 classId, uint256 nonceId, uint256 metadataId) external view returns ( Values memory); /** - * @dev Returns the information about the progress needed to redeem the bond + * @dev Returns the information about the progress needed to redeem the bond identified by classId and nonceId. * @notice Every bond contract can have its own logic concerning the progress definition. - * @param classId The class of bonds. + * @param classId The class of bonds. * @param nonceId is the nonce of bonds for finding the progress. * Returns progressAchieved is the current progress achieved. * Returns progressRemaining is the remaining progress. @@ -159,17 +163,17 @@ interface IERC3475 { function getProgress(uint256 classId, uint256 nonceId) external view returns (uint256 progressAchieved, uint256 progressRemaining); /** - * @notice Returns the amount which spender is still allowed to withdraw from _owner. + * @notice Returns the amount that spender is still allowed to withdraw from _owner (for given classId and nonceId issuance) * @param _owner is the address whose owner allocates some amount to the _spender address. - * @param classId is the classId of bond . + * @param classId is the classId of the bond. * @param nonceId is the nonce corresponding to the class for which you are approving the spending of total amount of bonds. */ function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external view returns (uint256); /** - * @notice Queries the approval status of an operator for a given owner. - * @param _owner is the current holder of the bonds for all classes / nonces. - * @param _operator is the address which is having access to the bonds of _owner for transferring - * Returns "true" if the operator is approved, "false" if not + * @notice Queries the approval status of an operator for bonds (for all classes and nonce issuances of owner). + * @param _owner is the current holder of the bonds for all classes/nonces. + * @param _operator is the address with access to the bonds of _owner for transferring. + * Returns "true" if the operator is approved, "false" if not. */ function isApprovedFor(address _owner, address _operator) external view returns (bool); diff --git a/assets/eip-4519/images/Fig5_rev.png b/assets/eip-4519/images/Fig5_rev.png deleted file mode 100644 index d9367774425cff..00000000000000 Binary files a/assets/eip-4519/images/Fig5_rev.png and /dev/null differ diff --git a/assets/eip-4519/images/Figure3.jpg b/assets/eip-4519/images/Figure3.jpg index 3342b4acf90a8a..dd0e0efa150552 100644 Binary files a/assets/eip-4519/images/Figure3.jpg and b/assets/eip-4519/images/Figure3.jpg differ diff --git a/assets/eip-4519/images/Figure4.jpg b/assets/eip-4519/images/Figure4.jpg index 2d7fb1f4e70bc3..62f1f81b0c95d1 100644 Binary files a/assets/eip-4519/images/Figure4.jpg and b/assets/eip-4519/images/Figure4.jpg differ diff --git a/assets/eip-4519/images/Figure5.jpg b/assets/eip-4519/images/Figure5.jpg new file mode 100644 index 00000000000000..1f8a1707e6e6ca Binary files /dev/null and b/assets/eip-4519/images/Figure5.jpg differ diff --git a/assets/eip-4675/contracts/ERC20Token.sol b/assets/eip-4675/contracts/ERC20Token.sol index 588e01045ffb9b..6b8f5faaccfb2c 100644 --- a/assets/eip-4675/contracts/ERC20Token.sol +++ b/assets/eip-4675/contracts/ERC20Token.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "./interface/IERC20.sol"; diff --git a/assets/eip-4675/contracts/MFNFT.sol b/assets/eip-4675/contracts/MFNFT.sol index c16c9dd4201143..098b4afdcd1398 100644 --- a/assets/eip-4675/contracts/MFNFT.sol +++ b/assets/eip-4675/contracts/MFNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "./interface/IMFNFT.sol"; diff --git a/assets/eip-4675/contracts/NFT.sol b/assets/eip-4675/contracts/NFT.sol index 048daec527725d..2dd6edbf96e296 100644 --- a/assets/eip-4675/contracts/NFT.sol +++ b/assets/eip-4675/contracts/NFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; diff --git a/assets/eip-4675/contracts/helper/Verifier.sol b/assets/eip-4675/contracts/helper/Verifier.sol index 0a7200241054f0..de3af4c1cbf863 100644 --- a/assets/eip-4675/contracts/helper/Verifier.sol +++ b/assets/eip-4675/contracts/helper/Verifier.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: ISC +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; contract Verifier { diff --git a/assets/eip-4675/contracts/interface/IERC20.sol b/assets/eip-4675/contracts/interface/IERC20.sol index 52224780e904e0..912040eceb0249 100644 --- a/assets/eip-4675/contracts/interface/IERC20.sol +++ b/assets/eip-4675/contracts/interface/IERC20.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: ISC +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; /** diff --git a/assets/eip-4675/contracts/interface/IERC721.sol b/assets/eip-4675/contracts/interface/IERC721.sol index f21735f392cfe1..ecc6976e6c5ba5 100644 --- a/assets/eip-4675/contracts/interface/IERC721.sol +++ b/assets/eip-4675/contracts/interface/IERC721.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; /** diff --git a/assets/eip-4675/contracts/interface/IMFNFT.sol b/assets/eip-4675/contracts/interface/IMFNFT.sol index 634fa1d679aa32..1c36139e2305f7 100644 --- a/assets/eip-4675/contracts/interface/IMFNFT.sol +++ b/assets/eip-4675/contracts/interface/IMFNFT.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: ISC +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; interface IMFNFT { diff --git a/assets/eip-4675/contracts/math/SafeMath.sol b/assets/eip-4675/contracts/math/SafeMath.sol deleted file mode 100644 index e70f5ea93322fd..00000000000000 --- a/assets/eip-4675/contracts/math/SafeMath.sol +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -/** - * @title SafeMath - * @dev Unsigned math operations with safety checks that revert on error - */ -library SafeMath { - /** - * @dev Multiplies two unsigned integers, reverts on overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 - if (a == 0) { - return 0; - } - - uint256 c = a * b; - require(c / a == b); - - return c; - } - - /** - * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - // Solidity only automatically asserts when dividing by 0 - require(b > 0); - uint256 c = a / b; - // assert(a == b * c + a % b); // There is no case in which this doesn't hold - - return c; - } - - /** - * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend). - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - require(b <= a); - uint256 c = a - b; - - return c; - } - - /** - * @dev Adds two unsigned integers, reverts on overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - uint256 c = a + b; - require(c >= a); - - return c; - } - - /** - * @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo), - * reverts when dividing by zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - require(b != 0); - return a % b; - } -} \ No newline at end of file diff --git a/assets/eip-4675/package.json b/assets/eip-4675/package.json index c220fb6c693c74..e5728bfe3b3f91 100644 --- a/assets/eip-4675/package.json +++ b/assets/eip-4675/package.json @@ -12,7 +12,7 @@ }, "keywords": [], "author": "", - "license": "ISC", + "license": "CC0-1.0", "bugs": { "url": "https://github.com/PowerStream3604/multi-fnft/issues" }, diff --git a/assets/eip-4973/ERC-4973.sol b/assets/eip-4973/ERC-4973.sol index c7cceef1051a67..4b7b60540a3bad 100644 --- a/assets/eip-4973/ERC-4973.sol +++ b/assets/eip-4973/ERC-4973.sol @@ -783,7 +783,7 @@ interface IERC721Metadata { /// @title Account-bound tokens /// @dev See https://eips.ethereum.org/EIPS/eip-4973 -/// Note: the ERC-165 identifier for this interface is 0x5164cf47 +/// Note: the ERC-165 identifier for this interface is 0x8d7bac72 interface IERC4973 { /// @dev This emits when ownership of any ABT changes by any mechanism. /// This event emits when ABTs are given or equipped and unequipped diff --git a/assets/eip-5027/0001-unlimit-code-size.patch b/assets/eip-5027/0001-unlimit-code-size.patch index da56b4107b2318..756bc321d0c0e0 100644 --- a/assets/eip-5027/0001-unlimit-code-size.patch +++ b/assets/eip-5027/0001-unlimit-code-size.patch @@ -1,17 +1,21 @@ -From 82ce60226a81e6a3acb92525482c16d254f63816 Mon Sep 17 00:00:00 2001 +From 7b8d4d1b8e00c0515ead0abb3f556e2b5a0617a7 Mon Sep 17 00:00:00 2001 From: Qi Zhou Date: Thu, 21 Apr 2022 11:35:27 -0700 -Subject: [PATCH] unlimit code size +Subject: [PATCH] unlimit code size with cold/warm storage --- - core/rawdb/accessors_state.go | 18 ++++++++++++++++++ - core/rawdb/schema.go | 6 ++++++ - core/state/database.go | 6 ++++++ - core/vm/evm.go | 6 +++--- - core/vm/gas_table.go | 25 +++++++++++++++++++++++++ - core/vm/instructions.go | 7 +++++++ - params/protocol_params.go | 2 +- - 7 files changed, 66 insertions(+), 4 deletions(-) + core/rawdb/accessors_state.go | 18 +++++++ + core/rawdb/schema.go | 6 +++ + core/state/access_list.go | 32 +++++++++++- + core/state/database.go | 6 +++ + core/state/journal.go | 11 ++++ + core/state/statedb.go | 23 ++++++-- + core/vm/eips.go | 20 +++++++ + core/vm/evm.go | 8 +-- + core/vm/interface.go | 2 + + core/vm/operations_acl.go | 98 +++++++++++++++++++++++++++++++++++ + params/protocol_params.go | 10 ++-- + 11 files changed, 221 insertions(+), 13 deletions(-) diff --git a/core/rawdb/accessors_state.go b/core/rawdb/accessors_state.go index 41e21b6ca..ad7fc150d 100644 @@ -80,11 +84,79 @@ index 08f373488..cbf1dc40f 100644 // IsCodeKey reports whether the given byte slice is the key of contract code, // if so return the raw code hash as well. func IsCodeKey(key []byte) (bool, []byte) { +diff --git a/core/state/access_list.go b/core/state/access_list.go +index 419469134..22812a936 100644 +--- a/core/state/access_list.go ++++ b/core/state/access_list.go +@@ -21,8 +21,9 @@ import ( + ) + + type accessList struct { +- addresses map[common.Address]int +- slots []map[common.Hash]struct{} ++ addresses map[common.Address]int ++ codeInAddresses map[common.Address]bool ++ slots []map[common.Hash]struct{} + } + + // ContainsAddress returns true if the address is in the access list. +@@ -31,6 +32,12 @@ func (al *accessList) ContainsAddress(address common.Address) bool { + return ok + } + ++// ContainsAddress returns true if the address is in the access list. ++func (al *accessList) ContainsAddressCode(address common.Address) bool { ++ _, ok := al.codeInAddresses[address] ++ return ok ++} ++ + // Contains checks if a slot within an account is present in the access list, returning + // separate flags for the presence of the account and the slot respectively. + func (al *accessList) Contains(address common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { +@@ -60,6 +67,9 @@ func (a *accessList) Copy() *accessList { + for k, v := range a.addresses { + cp.addresses[k] = v + } ++ for k, v := range a.codeInAddresses { ++ cp.codeInAddresses[k] = v ++ } + cp.slots = make([]map[common.Hash]struct{}, len(a.slots)) + for i, slotMap := range a.slots { + newSlotmap := make(map[common.Hash]struct{}, len(slotMap)) +@@ -81,6 +91,16 @@ func (al *accessList) AddAddress(address common.Address) bool { + return true + } + ++// AddAddressCode adds the code of an address to the access list, and returns 'true' if the operation ++// caused a change (addr was not previously in the list). ++func (al *accessList) AddAddressCode(address common.Address) bool { ++ if _, present := al.codeInAddresses[address]; present { ++ return false ++ } ++ al.codeInAddresses[address] = true ++ return true ++} ++ + // AddSlot adds the specified (addr, slot) combo to the access list. + // Return values are: + // - address added +@@ -134,3 +154,11 @@ func (al *accessList) DeleteSlot(address common.Address, slot common.Hash) { + func (al *accessList) DeleteAddress(address common.Address) { + delete(al.addresses, address) + } ++ ++// DeleteAddressCode removes the code of an address from the access list. This operation ++// needs to be performed in the same order as the addition happened. ++// This method is meant to be used by the journal, which maintains ordering of ++// operations. ++func (al *accessList) DeleteAddressCode(address common.Address) { ++ delete(al.codeInAddresses, address) ++} diff --git a/core/state/database.go b/core/state/database.go index bbcd2358e..7445e627f 100644 --- a/core/state/database.go +++ b/core/state/database.go -@@ -194,6 +194,12 @@ func (db *cachingDB) ContractCodeSize(addrHash, codeHash common.Hash) (int, error +@@ -194,6 +194,12 @@ func (db *cachingDB) ContractCodeSize(addrHash, codeHash common.Hash) (int, erro if cached, ok := db.codeSizeCache.Get(codeHash); ok { return cached.(int), nil } @@ -97,11 +169,149 @@ index bbcd2358e..7445e627f 100644 code, err := db.ContractCode(addrHash, codeHash) return len(code), err } +diff --git a/core/state/journal.go b/core/state/journal.go +index 57a692dc7..8e2250dde 100644 +--- a/core/state/journal.go ++++ b/core/state/journal.go +@@ -134,6 +134,9 @@ type ( + accessListAddAccountChange struct { + address *common.Address + } ++ accessListAddAccountCodeChange struct { ++ address *common.Address ++ } + accessListAddSlotChange struct { + address *common.Address + slot *common.Hash +@@ -260,6 +263,14 @@ func (ch accessListAddAccountChange) dirtied() *common.Address { + return nil + } + ++func (ch accessListAddAccountCodeChange) revert(s *StateDB) { ++ s.accessList.DeleteAddressCode(*ch.address) ++} ++ ++func (ch accessListAddAccountCodeChange) dirtied() *common.Address { ++ return nil ++} ++ + func (ch accessListAddSlotChange) revert(s *StateDB) { + s.accessList.DeleteSlot(*ch.address, *ch.slot) + } +diff --git a/core/state/statedb.go b/core/state/statedb.go +index 1d31cf470..d95dd79aa 100644 +--- a/core/state/statedb.go ++++ b/core/state/statedb.go +@@ -984,11 +984,11 @@ func (s *StateDB) Commit(deleteEmptyObjects bool) (common.Hash, error) { + } + + // PrepareAccessList handles the preparatory steps for executing a state transition with +-// regards to both EIP-2929 and EIP-2930: ++// regards to both EIP-2929, EIP-2930, and EIP-5027: + // +-// - Add sender to access list (2929) +-// - Add destination to access list (2929) +-// - Add precompiles to access list (2929) ++// - Add sender to access list (2929, 5027) ++// - Add destination to access list (2929, 5027) ++// - Add precompiles to access list (2929, 5027) + // - Add the contents of the optional tx access list (2930) + // + // This method should only be called if Berlin/2929+2930 is applicable at the current number. +@@ -997,12 +997,15 @@ func (s *StateDB) PrepareAccessList(sender common.Address, dst *common.Address, + s.accessList = newAccessList() + + s.AddAddressToAccessList(sender) ++ s.AddAddressCodeToAccessList(sender) + if dst != nil { + s.AddAddressToAccessList(*dst) ++ s.AddAddressCodeToAccessList(*dst) + // If it's a create-tx, the destination will be added inside evm.create + } + for _, addr := range precompiles { + s.AddAddressToAccessList(addr) ++ s.AddAddressCodeToAccessList(addr) + } + for _, el := range list { + s.AddAddressToAccessList(el.Address) +@@ -1019,6 +1022,13 @@ func (s *StateDB) AddAddressToAccessList(addr common.Address) { + } + } + ++// AddAddressCodeToAccessList adds the given address to the access list ++func (s *StateDB) AddAddressCodeToAccessList(addr common.Address) { ++ if s.accessList.AddAddressCode(addr) { ++ s.journal.append(accessListAddAccountCodeChange{&addr}) ++ } ++} ++ + // AddSlotToAccessList adds the given (address, slot)-tuple to the access list + func (s *StateDB) AddSlotToAccessList(addr common.Address, slot common.Hash) { + addrMod, slotMod := s.accessList.AddSlot(addr, slot) +@@ -1042,6 +1052,11 @@ func (s *StateDB) AddressInAccessList(addr common.Address) bool { + return s.accessList.ContainsAddress(addr) + } + ++// AddressCodeInAccessList returns true if the given address's code is in the access list. ++func (s *StateDB) AddressCodeInAccessList(addr common.Address) bool { ++ return s.accessList.ContainsAddressCode(addr) ++} ++ + // SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. + func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { + return s.accessList.Contains(addr, slot) +diff --git a/core/vm/eips.go b/core/vm/eips.go +index 4070a2db5..e9a8ee78c 100644 +--- a/core/vm/eips.go ++++ b/core/vm/eips.go +@@ -31,6 +31,7 @@ var activators = map[int]func(*JumpTable){ + 2200: enable2200, + 1884: enable1884, + 1344: enable1344, ++ 5027: enable5027, + } + + // EnableEIP enables the given EIP on the config. +@@ -147,6 +148,25 @@ func enable2929(jt *JumpTable) { + jt[SELFDESTRUCT].dynamicGas = gasSelfdestructEIP2929 + } + ++// enable2929 enables "EIP-2929: Gas cost increases for state access opcodes" ++// https://eips.ethereum.org/EIPS/eip-2929 ++func enable5027(jt *JumpTable) { ++ jt[EXTCODECOPY].constantGas = params.WarmStorageReadCostEIP2929 ++ jt[EXTCODECOPY].dynamicGas = gasExtCodeCopyEIP5027 ++ ++ jt[CALL].constantGas = params.WarmStorageReadCostEIP2929 ++ jt[CALL].dynamicGas = gasCallEIP5027 ++ ++ jt[CALLCODE].constantGas = params.WarmStorageReadCostEIP2929 ++ jt[CALLCODE].dynamicGas = gasCallCodeEIP5027 ++ ++ jt[STATICCALL].constantGas = params.WarmStorageReadCostEIP2929 ++ jt[STATICCALL].dynamicGas = gasStaticCallEIP5027 ++ ++ jt[DELEGATECALL].constantGas = params.WarmStorageReadCostEIP2929 ++ jt[DELEGATECALL].dynamicGas = gasDelegateCallEIP5027 ++} ++ + // enable3529 enabled "EIP-3529: Reduction in refunds": + // - Removes refunds for selfdestructs + // - Reduces refunds for SSTORE diff --git a/core/vm/evm.go b/core/vm/evm.go -index dd55618bf..5dc3ed6ca 100644 +index dd55618bf..99e57c28e 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go -@@ -453,9 +453,9 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, +@@ -421,6 +421,8 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, + // the access-list change should not be rolled back + if evm.chainRules.IsBerlin { + evm.StateDB.AddAddressToAccessList(address) ++ // TODO: check shanghai ++ evm.StateDB.AddAddressCodeToAccessList(address) + } + // Ensure there's no existing contract already at the designated address + contractHash := evm.StateDB.GetCodeHash(address) +@@ -453,9 +455,9 @@ func (evm *EVM) create(caller ContractRef, codeAndHash *codeAndHash, gas uint64, ret, err := evm.interpreter.Run(contract, nil, false) // Check whether the max code size has been exceeded, assign err if the case. @@ -114,108 +324,169 @@ index dd55618bf..5dc3ed6ca 100644 // Reject code starting with 0xEF if EIP-3541 is enabled. if err == nil && len(ret) >= 1 && ret[0] == 0xEF && evm.chainRules.IsLondon { -diff --git a/core/vm/gas_table.go b/core/vm/gas_table.go -index 4c2cb3e5c..d0118b1ee 100644 ---- a/core/vm/gas_table.go -+++ b/core/vm/gas_table.go -@@ -325,6 +325,16 @@ func gasExpEIP158(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory +diff --git a/core/vm/interface.go b/core/vm/interface.go +index ad9b05d66..12660dd08 100644 +--- a/core/vm/interface.go ++++ b/core/vm/interface.go +@@ -59,6 +59,7 @@ type StateDB interface { + + PrepareAccessList(sender common.Address, dest *common.Address, precompiles []common.Address, txAccesses types.AccessList) + AddressInAccessList(addr common.Address) bool ++ AddressCodeInAccessList(addr common.Address) bool + SlotInAccessList(addr common.Address, slot common.Hash) (addressOk bool, slotOk bool) + // AddAddressToAccessList adds the given address to the access list. This operation is safe to perform + // even if the feature/fork is not active yet +@@ -66,6 +67,7 @@ type StateDB interface { + // AddSlotToAccessList adds the given (address,slot) to the access list. This operation is safe to perform + // even if the feature/fork is not active yet + AddSlotToAccessList(addr common.Address, slot common.Hash) ++ AddAddressCodeToAccessList(addr common.Address) + + RevertToSnapshot(int) + Snapshot() int +diff --git a/core/vm/operations_acl.go b/core/vm/operations_acl.go +index 551e1f5f1..cb76a4390 100644 +--- a/core/vm/operations_acl.go ++++ b/core/vm/operations_acl.go +@@ -138,6 +138,41 @@ func gasExtCodeCopyEIP2929(evm *EVM, contract *Contract, stack *Stack, mem *Memo return gas, nil } -+func addGasExtraCodeSize(evm *EVM, address common.Address, gas uint64) (uint64, bool) { -+ codeSize := evm.StateDB.GetCodeSize(address) -+ if codeSize <= params.CodeSizeUnit { -+ return gas, false ++// gasExtCodeCopyEIP5027 implements extcodecopy according to EIP-5027 ++// EIP spec: ++// > If the target is not in accessed_addresses, ++// > charge COLD_ACCOUNT_ACCESS_COST * N_CODE_UNIT gas, and add the address to accessed_addresses and accessed_code_in_addresses. ++// > Else if the target is not in accessed_code_in_addresses, ++// > charge COLD_ACCOUNT_ACCESS_COST * (N_CODE_UNIT - 1) gas, and add the address to accessed_code_in_addresses. ++// > Otherwise, charge WARM_STORAGE_READ_COST gas. ++func gasExtCodeCopyEIP5027(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { ++ // memory expansion first (dynamic part of pre-5027 implementation) ++ gas, err := gasExtCodeCopy(evm, contract, stack, mem, memorySize) ++ if err != nil { ++ return 0, err ++ } ++ addr := common.Address(stack.peek().Bytes20()) ++ // Check slot presence in the access list ++ if !evm.StateDB.AddressInAccessList(addr) { ++ evm.StateDB.AddAddressToAccessList(addr) ++ var overflow bool ++ // We charge (cold-warm), since 'warm' is already charged as constantGas ++ if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929-params.WarmStorageReadCostEIP2929); overflow { ++ return 0, ErrGasUintOverflow ++ } + } ++ if !evm.StateDB.AddressCodeInAccessList(addr) { ++ evm.StateDB.AddAddressCodeToAccessList(addr) ++ var overflow bool + -+ extraGas := (uint64(codeSize) - 1) / params.CodeSizeUnit * params.CallGasEIP150 -+ return math.SafeAdd(gas, extraGas) ++ // We charge cold for extra code ++ if gas, overflow = math.SafeAdd(gas, params.ColdAccountAccessCostEIP2929*getExtraCodeUnit(evm, addr)); overflow { ++ return 0, ErrGasUintOverflow ++ } ++ } ++ return gas, nil +} + - func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { - var ( - gas uint64 -@@ -357,6 +367,9 @@ func gasCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { - return 0, ErrGasUintOverflow - } -+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow { -+ return 0, ErrGasUintOverflow -+ } - return gas, nil - } - -@@ -368,6 +381,7 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory - var ( - gas uint64 - overflow bool -+ address = common.Address(stack.Back(1).Bytes20()) - ) - if stack.Back(2).Sign() != 0 { - gas += params.CallValueTransferGas -@@ -382,10 +396,14 @@ func gasCallCode(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memory - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { - return 0, ErrGasUintOverflow - } -+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow { -+ return 0, ErrGasUintOverflow -+ } - return gas, nil - } - - func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { -+ address := common.Address(stack.Back(1).Bytes20()) - gas, err := memoryGasCost(mem, memorySize) - if err != nil { - return 0, err -@@ -398,10 +416,14 @@ func gasDelegateCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, me - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { - return 0, ErrGasUintOverflow + // gasEip2929AccountCheck checks whether the first stack item (as address) is present in the access list. + // If it is, this method returns '0', otherwise 'cold-warm' gas, presuming that the opcode using it + // is also using 'warm' as constant factor. +@@ -191,6 +226,64 @@ func makeCallVariantGasCallEIP2929(oldCalculator gasFunc) gasFunc { } -+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow { -+ return 0, ErrGasUintOverflow -+ } - return gas, nil } - func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { -+ address := common.Address(stack.Back(1).Bytes20()) - gas, err := memoryGasCost(mem, memorySize) - if err != nil { - return 0, err -@@ -414,6 +436,9 @@ func gasStaticCall(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memo - if gas, overflow = math.SafeAdd(gas, evm.callGasTemp); overflow { - return 0, ErrGasUintOverflow - } -+ if gas, overflow = addGasExtraCodeSize(evm, address, gas); overflow { -+ return 0, ErrGasUintOverflow -+ } - return gas, nil - } - -diff --git a/core/vm/instructions.go b/core/vm/instructions.go -index db507c481..f1a6112bd 100644 ---- a/core/vm/instructions.go -+++ b/core/vm/instructions.go -@@ -383,6 +383,13 @@ func opExtCodeCopy(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) - uint64CodeOffset = 0xffffffffffffffff - } - addr := common.Address(a.Bytes20()) -+ codeSize := interpreter.evm.StateDB.GetCodeSize(addr) ++func getExtraCodeUnit(evm *EVM, addr common.Address) uint64 { ++ codeSize := evm.StateDB.GetCodeSize(addr) ++ extraCodeUnit := uint64(0) + if codeSize > params.CodeSizeUnit { -+ extraGas := (uint64(codeSize - 1)) / params.CodeSizeUnit * params.ExtcodeSizeGasEIP150 -+ if !scope.Contract.UseGas(extraGas) { -+ return nil, ErrOutOfGas ++ extraCodeUnit = (uint64(codeSize - 1)) / params.CodeSizeUnit ++ } ++ return extraCodeUnit ++} ++ ++func makeCallVariantGasCallEIP5027(oldCalculator gasFunc) gasFunc { ++ return func(evm *EVM, contract *Contract, stack *Stack, mem *Memory, memorySize uint64) (uint64, error) { ++ addr := common.Address(stack.Back(1).Bytes20()) ++ // Check slot presence in the access list ++ warmAccess := evm.StateDB.AddressInAccessList(addr) ++ warmCodeAccess := evm.StateDB.AddressCodeInAccessList(addr) ++ // The WarmStorageReadCostEIP2929 (100) is already deducted in the form of a constant cost, so ++ // the cost to charge for cold access, if any, is n * Cold - Warm ++ coldCost := params.ColdAccountAccessCostEIP2929 - params.WarmStorageReadCostEIP2929 ++ ++ if !warmAccess { ++ evm.StateDB.AddAddressToAccessList(addr) ++ evm.StateDB.AddAddressCodeToAccessList(addr) ++ ++ coldCost += getExtraCodeUnit(evm, addr) * params.ColdAccountCodeAccessCostEIP5027 ++ ++ // Charge the remaining difference here already, to correctly calculate available ++ // gas for call ++ if !contract.UseGas(coldCost) { ++ return 0, ErrOutOfGas ++ } ++ } else if !warmCodeAccess { ++ evm.StateDB.AddAddressCodeToAccessList(addr) ++ ++ coldCost = getExtraCodeUnit(evm, addr) * params.ColdAccountCodeAccessCostEIP5027 ++ // Charge the remaining difference here already, to correctly calculate available ++ // gas for call ++ if !contract.UseGas(coldCost) { ++ return 0, ErrOutOfGas ++ } ++ } ++ // Now call the old calculator, which takes into account ++ // - create new account ++ // - transfer value ++ // - memory expansion ++ // - 63/64ths rule ++ gas, err := oldCalculator(evm, contract, stack, mem, memorySize) ++ if (warmAccess && warmCodeAccess) || err != nil { ++ return gas, err + } ++ // In case of a cold access, we temporarily add the cold charge back, and also ++ // add it to the returned gas. By adding it to the return, it will be charged ++ // outside of this function, as part of the dynamic gas, and that will make it ++ // also become correctly reported to tracers. ++ contract.Gas += coldCost ++ return gas + coldCost, nil + } - codeCopy := getData(interpreter.evm.StateDB.GetCode(addr), uint64CodeOffset, length.Uint64()) - scope.Memory.Set(memOffset.Uint64(), length.Uint64(), codeCopy) ++} ++ + var ( + gasCallEIP2929 = makeCallVariantGasCallEIP2929(gasCall) + gasDelegateCallEIP2929 = makeCallVariantGasCallEIP2929(gasDelegateCall) +@@ -200,6 +293,11 @@ var ( + // gasSelfdestructEIP3529 implements the changes in EIP-2539 (no refunds) + gasSelfdestructEIP3529 = makeSelfdestructGasFn(false) ++ gasCallEIP5027 = makeCallVariantGasCallEIP5027(gasCall) ++ gasDelegateCallEIP5027 = makeCallVariantGasCallEIP5027(gasDelegateCall) ++ gasStaticCallEIP5027 = makeCallVariantGasCallEIP5027(gasStaticCall) ++ gasCallCodeEIP5027 = makeCallVariantGasCallEIP5027(gasCallCode) ++ + // gasSStoreEIP2929 implements gas cost for SSTORE according to EIP-2929 + // + // When calling SSTORE, check if the (address, storage_key) pair is in accessed_storage_keys. diff --git a/params/protocol_params.go b/params/protocol_params.go -index 5f154597a..c30b54da8 100644 +index 5f154597a..c3d5c66ce 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go -@@ -123,7 +123,7 @@ const ( +@@ -58,9 +58,11 @@ const ( + SstoreResetGasEIP2200 uint64 = 5000 // Once per SSTORE operation from clean non-zero to something else + SstoreClearsScheduleRefundEIP2200 uint64 = 15000 // Once per SSTORE operation for clearing an originally existing storage slot + +- ColdAccountAccessCostEIP2929 = uint64(2600) // COLD_ACCOUNT_ACCESS_COST +- ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST +- WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST ++ ColdAccountAccessCostEIP2929 = uint64(2600) // COLD_ACCOUNT_ACCESS_COST ++ ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST ++ WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST ++ ColdAccountCodeAccessCostEIP5027 = uint64(2600) // COLD_ACCOUNT_CODE_ACCESS_COST_PER_UNIT ++ WarmAccountCodeAccessCostEIP5027 = uint64(2600) // WARM_ACCOUNT_CODE_ACCESS_COST_PER_UNIT + + // In EIP-2200: SstoreResetGas was 5000. + // In EIP-2929: SstoreResetGas was changed to '5000 - COLD_SLOAD_COST'. +@@ -123,7 +125,7 @@ const ( ElasticityMultiplier = 2 // Bounds the maximum gas limit an EIP-1559 block may have. InitialBaseFee = 1000000000 // Initial base fee for EIP-1559 blocks. diff --git a/assets/eip-5218/ic3license/ic3license.pdf b/assets/eip-5218/ic3license/ic3license.pdf index 1bac5933a6e2ad..f6fb07f3e17784 100644 Binary files a/assets/eip-5218/ic3license/ic3license.pdf and b/assets/eip-5218/ic3license/ic3license.pdf differ diff --git a/assets/eip-5218/ic3license/ic3license.tex b/assets/eip-5218/ic3license/ic3license.tex index 6367a06cf86645..c38d2f3c77dc7c 100644 --- a/assets/eip-5218/ic3license/ic3license.tex +++ b/assets/eip-5218/ic3license/ic3license.tex @@ -7,7 +7,7 @@ \title{The \iccclicense\\} \author{James Grimmelmann} \affil{Cornell Tech and Cornell Law School\\Institute for Cryptocurrencies and Contracts} -\date{DISCUSSION DRAFT 1\\\today} +\date{DISCUSSION DRAFT 3\\\today} \usepackage{xspace} @@ -16,15 +16,24 @@ \newcommand{\keyword}[1]{\textbf{#1}\xspace} +\newcommand{\publiclicense}{\keyword{Public-License}} +\newcommand{\nopubliclicense}{\keyword{No-Public-License}} + \newcommand{\commercial}{\keyword{Commercial}} \newcommand{\noncommercial}{\keyword{Non-Commercial}} -\newcommand{\derivative}{\keyword{Derivatives}} \newcommand{\noderivative}{\keyword{No-Derivatives}} +\newcommand{\derivative}{\keyword{Derivatives}} +\newcommand{\sharealike}{\keyword{Share-Alike}} +\newcommand{\derivativetracking}{\keyword{Derivative-Tracking}} \newcommand{\ledger}{\keyword{Ledger-Authoritative}} \newcommand{\legal}{\keyword{Legal-Authoritative}} +\newcommand{\personal}{\keyword{Personal}} +\newcommand{\conditional}{\keyword{Conditional}} + + \usepackage{semantic-markup} \newcommand{\sect}[1]{\vspace{12pt}\noindent{\strong{#1}}} @@ -38,6 +47,7 @@ \renewcommand{\code}[1]{\colorbox{light-gray}{\texttt{#1}}} \newcommand{\iflicenseoption}[2]{[\colorbox{light-gray}{If #1:} #2]} +\newcommand{\ifnotlicenseoption}[2]{[\colorbox{light-gray}{Unless #1:} #2]} \begin{document} @@ -49,13 +59,14 @@ \section{Introduction} The \iccclicense is a copyright license specifically designed for use with NFTs. It links a creative work to an NFT so that when the NFT is transferred, so is the license. It is intended to be compatible with the NFT licensing standard defined in \eiplicense. -The \iccclicense has three optional features that a licensor can use or not when choosing a license: +The \iccclicense has four optional features that a licensor can use or not when choosing a license: \begin{itemize} -\item The license can be \commercial or \noncommercial. A \commercial license allows the user to make money from merchandise and other uses of the work; a \noncommercial license does not. -\item The license can be \derivative or \noderivative. A \derivative license allows the user to make derivative works and adaptations, like remixes and video series; a \noderivative license does not. -\item The license can be \ledger or \legal. A \ledger license means that the current state of the blockchain ledger is always authoritative for who owns the NFT and has rights under the license, even if the NFT is stolen or transferred by mistake. A \legal license gives the courts flexibility to correct ownership of the license in clear cases of theft and fraud. +\item The license can be \commercial (C) or \noncommercial (NC). A \commercial license allows the user to make money from merchandise and other uses of the work; a \noncommercial license does not. +\item The license can be \derivative (D), \derivativetracking (DT), \sharealike (SA), or \noderivative (ND). A \derivative license allows the user to make derivative works and adaptations (like remixes and videos) without restriction. A \derivativetracking license allows derivative works but requires them to be registered and tracked as NFTs themselves. A \sharealike license is like \derivativetracking but also requires that the derivative works be relicensed under the same license. And a \noderivative license prohibits derivative works entirely. (These options are ordered from least restrictive to most restrictive.) +\item The license can be \publiclicense (PL) or \nopubliclicense (NPL). A \publiclicense license, in addition to the specific rights granted to the user, grants to the public a broad copyright license to reproduce the work (but not to make derivative works). A \nopubliclicense license grants only the specific rights to the user. +\item The license can be \ledger (Ledger) or \legal (Legal). A \ledger license means that the current state of the blockchain ledger is always authoritative for who owns the NFT and has rights under the license, even if the NFT is stolen or transferred by mistake. A \legal license gives the courts flexibility to correct ownership of the license in clear cases of theft and fraud. \end{itemize} -All three options are independent, so that they can be set for a choice of eight total licenses, e.g. NC-D-Ledger means the license version that allows only Non-Commercial uses, allows Deriviative works, and makes the Ledger authoritative for the state of the license. Once an \iccclicense has been used on an NFT, that license stays with it. The licensor should not in general expect to be able to revoke the license or choose a different one. +All four options are independent, so that they can be set for a choice of thirty-two total licenses, e.g. NC-D-NPL-Ledger means the license version that allows only Non-Commercial uses, allows Deriviative works, includes No Public License, and makes the Ledger authoritative for the state of the license. Once an \iccclicense has been used on an NFT, that license stays with it. The licensor should not in general expect to be able to revoke the license or choose a different one. The rest of this document describes the design choices of the \iccclicense. Appendix \ref{sec:human} provides a short human-readable summary of the license terms. Appendix \ref{sec:text} contains the actual text of the license family; @@ -94,23 +105,46 @@ \section{License Terms} We have tried in \iccclicense to capture the most common and important use cases in light of the NFT community's responses to previous licenses. But our goal has been simplicity rather than perfection. We hope and expect that over time, others will build on and remix the ideas in the license to terms for more advanced use cases. -Drawing on the success of the Creative Commons license suite, the \iccclicense can be customized to be either \commercial or \noncommercial (C vs. NC), and to allow \derivative works or to specify \noderivative works (D vs. ND). These two options can be set independently. (A third variation, \ledger versus \legal, is discussed in \ref{sec:trans}.) We did not include a Creative Commons-style ShareAlike option for derivative works. A licensor who is interested in establishing a shared pool of creative works should use an open license that lets anyone make derivative works (such as the CC BY-SA license or the GPL), rather than a license tied to an NFT. - The \iccclicense applies to ``Licensed Material'' that is ``Linked'' to an NFT. The definition of Licensed Material is deliberately broad: it can include highly creative works like images or videos, but it also includes ``other material'' to include datasets, software, or other works that are not primarily artistic. The definition of Linked is also broad: it can be by hyperlink, by description, by IPFS CID, by hash value, by an on-chain reference, etc. What is important is that the NFT must be connected to specific licensed material in a way that cannot change over time. We do not attempt to solve the (very difficult) problems of creating a technical standard or a license for an NFT whose contents can change over time. The core license grant in the \iccclicense is that the NFT owner can ``Use'' the licensed material, i.e. exercise all of the usual rights under copyright to make copies of the work and share them with the public. This is an unlimited grant: it covers all media, digital and analog, on-chain and off-chain. The license grant is non-exclusive. It is not currently possible to guarantee that only one NFT has been made of a work, or that there are no other off-chain uses of the work. +Drawing on the success of the Creative Commons license suite, the \iccclicense can be customized in four ways: +\begin{enumerate} +\item To be either \commercial or \noncommercial. +\item To allow \derivative works, to allow them but require that they be \derivativetracking on chain, to allow them but require that they be relicensed under \sharealike terms, or to specify that \noderivative works at all. +\item To grant a \publiclicense to members of the public besides the NFT owner, or to grant \nopubliclicense. +\item To track ownership of the NFT entirely on-chain (\ledger) or to follow the legal system's rules on ownership (\legal). (This option is discussed in section~\ref{sec:trans}.) +\end{enumerate} +These four options can be set independently, for a total of 32 license variations. + +Regardless of which license options are used, all versions of the \iccclicense include two specific uses that are always allowed. First, the material can be used to sell the NFT, e.g. on an online NFT marketplace listing. This is an essential part of truth in advertising; someone considering buying the NFT typically needs to be able to see what creative work they will actually be getting. A similar clause is present in many other NFT licenses, although we have attempted to generalize it so that it is less tied to the specifics of how any particular NFT marketplace works. Second, the \iccclicense generally allows free use of the material to identify the NFT owner publicly, e.g. in a Twitter profile hexagon. This too is widespread in the NFT community and widely allowed by other NFT licenses. + + +\subsection{Commercial Uses} + If the license is \noncommercial, it excludes any uses directed to ``commercial advantage or monetary compensation,'' which includes any cases in which people are required to pay to get a copy of the work. The definition of the \noncommercial license option specifically \emph{allows} the sale of the NFT itself. The resale of NFTs, like the resale of unique works of fine art, is a recognized and important use case. What the NonCommercial license option prohibits is the commercialization of the work by making and selling \emph{other} copies of the work, such as selling posters of an image attached to an NFT, or creating a video series based on a character depicted in a work associated with an NFT. Whether the license is \commercial or \noncommercial, no royalties are required. Under the \commercial license option, the NFT owner is allowed to keep all of their revenues from commercializing the work. Under the \noncommercial license option, such commercialization is not allowed at all, and constitutes a breach of the license allowing the licensor to sue for infringement. Royalties pose complicated computation and drafting problems, which we have not attempted to solve in the current draft. If a platform charges a fee for an NFT listing or sale, this is neither required nor prohibited by the \iccclicense; it is an issue between the NFT owner and the platform, not between the NFT owner and the licensor. Similarly, if the NFT owner sells the NFT, this is not an event that entitles the NFT owner to any royalties under this license. This does not prevent the \iccclicense from being used with a smart contract that requires royalty payments. We merely have not attempted to make payment of royalties part of the copyright license. For example, a smart contract could tie invocation of NFT transfer functions to payment of required royalties. (But see EIP-2981 (NFT Royalty Standard) for discussion of the reasons why such requirements may not be effective in practice.) -If the licensor chooses to allow derivative works, then the license grant also allows the NFT owner to use ``Adapted Material,'' which uses language from the Creative Commons license suite to define what counts as a derivative work. To reflect the customs of the NFT community, we added language to make clear that simply reproducing the work in a different medium -- e.g., printing T-shirts of a JPEG -- doesn't count as making a derivative work. Only new projects -- such as modifying artwork, remixing a song, or making a TV series based on a character -- are derivative works. +\subsection{Derivative Works} + +If the licensor chooses to allow derivative works with \derivative, \derivativetracking, or \sharealike, the license grant also allows the NFT owner to use ``Adapted Material,'' which uses language from the Creative Commons license suite to define what counts as a derivative work. To reflect the customs of the NFT community, we added language to make clear that simply reproducing the work in a different medium -- e.g., printing T-shirts of a JPEG -- doesn't count as making a derivative work. Only new projects -- such as modifying artwork, remixing a song, or making a TV series based on a character -- are derivative works. + +When the license requires \derivativetracking or \sharealike, the grant of permission to make derivative works is conditional on using the same technical mechanism (an ``NFT Generator'') that created the NFT to also create an NFT for the derivative work. These two options implement a \emph{technical} compatibility condition, one that aims to keep derivative works on the same technical standard as the NFT itself. The license is platform-agnostic, so it can be used with different smart contracts, different blockchains, and even non-blockchain systems. The point is simply that when one of these license options is selected, the derivative-work NFTs will live in the same ecosystem used for the NFT. + +When the license requires \sharealike, the requirement of using the same technical mechanism is supplemented with the further requirement that the derivative-work NFT be licensed using the same license. This is a \emph{legal} compatibility condition; it closely corresponds to the Share-Alike license option in the Creative Commons license suite. Note that \sharealike implies \derivativetracking. In the context of NFTs, the point of a Share-Alike license could be defeated by tying the license to an derivative-work NFT whose technical implementation departed too significantly from the implementation of the underlying NFT. Rather than try to prohibit the use of what the Creative Commons suite calls ``Effective Technical Measures'' and take on the difficult task of defining them in the context of NFTs, we have chosen simply to require use of the same implementation for the derivative-work NFT, on the theory that the original NFT creator can choose an appropriate implementation at the time of minting. + +\subsection{Public Licenses} + +One common use case for NFTs is to reserve a few privileges for the NFT owner while giving the public a license to use the creative work associated with the NFT. This could be accomplished through dual licensing, in which the creator explicitly uses one license for the NFT owner and another for the public. But this approach is unsatisfactory; dual licensing gives up some of the convenience and clarity of having all of the relevant license terms clearly located in one place. Nor is it appropriate to include a public license in every version of the NFT license. The public-license model for NFTs is not universal; many NFTs give rights in the creative work only to the NFT owner. + +Thus, the IC3 NFT License includes a public license as a license option. When \publiclicense is selected, the license grants described above, which give specific broad rights to the NFT owner, are accompanied by a public license grant (specifically, a Creative Commons Attribution-NoDerivatives license) that gives the public the right to use the work, but not to make derivative works. -Regardless of which license option is used, all versions of the \iccclicense include two specific uses that are always allowed. First, the material can be used to sell the NFT, e.g. on an online NFT marketplace listing. This is an essential part of truth in advertising; someone considering buying the NFT typically needs to be able to see what creative work they will actually be getting. A similar clause is present in many other NFT licenses, although we have attempted to generalize it so that it is less tied to the specifics of how any particular NFT marketplace works. Second, the \iccclicense generally allows free use of the material to identify the NFT owner publicly, e.g. in a Twitter profile hexagon. This too is widespread in the NFT community and widely allowed by other NFT licenses. \section{Transfer} @@ -128,6 +162,8 @@ \section{Transfer} To keep the distinction clear, the license uses the term ``Controller'' for the person who has control of an NFT via a private key, and ``Owner'' for the person who is legally entitled to the license. The \ledger option is implemented by defining the Owner to be the Controller (so the two concepts are the same), and the \legal option by defining the Owner to be the ``person or entity who is legally entitled to be its Controller.'' (In theory, it would be possible for a license to fine-tune the circumstances under which it does and does not transfer by stating its own rules, but at the cost of decreased compatibility with both the blockchain and with the legal system. The \iccclicense does not attempt this task.) +The licensor may optionally provide a ``Grace Period'' as part of the smart contract for the NFT. If present, it allows a NFT owner to continue using the work (but not to create new derivatives of it) for a period of time following the transfer of the NFT. The Grace Period is defined in terms of a ``Grace Period Process,'' e.g. a smart-contract method that indicates whether the grace period has expired or no. In our view, this is the most general solution to the question of how long a grace period should be, if any. The licensor is not locked in to any particular choice, and putting this function in the smart-contract logic avoids any difficulties translating human-readable terms like ``two days'' into computation-friendly form. + \section{Revocation} The \iccclicense can be revoked, but it does not define the conditions of revocation. This may seem paradoxical, but it reflects the design goal of making the license itself simple and modular. Instead, the license \emph{defers} to the smart contract that invoked it in the first place. If that contract says that the license has been revoked, it has been. @@ -171,7 +207,7 @@ \section{Human-Readable Summary} \begin{itemize} \item \textbf{Use and Share}: You can make copies of the work, use them personally, and share them with others \iflicenseoption{\noncommercial}{, but not for commercial purposes}. -\item \iflicenseoption{\derivative}{\textbf{Derivatives}: you can make new works that include and build on the work, use them, and share them with others \iflicenseoption{\noncommercial}{, but not for commercial purposes}.} +\item \ifnotlicenseoption{\noderivative}{\textbf{Derivatives}: you can make new works that include and build on the work, use them, and share them with others \iflicenseoption{\noncommercial}{, but not for commercial purposes} \iflicenseoption{\derivativetracking}{, provided that you also release them as NFTs} \iflicenseoption{\sharealike}{, provided that you also release them as NFTs under the same license}.} \item \textbf{Ownership}: You can use the work to show that you own the NFT. \item \textbf{Sale}: If you sell the NFT, you can use the work to show people what the NFT is. \end{itemize} @@ -201,11 +237,16 @@ \section{License Text} \item A ``Ledger'' is a blockchain, database, or other digital system that records information about Unique Identifiers. + \item An ``NFT'' is a Unique Identifier in a Ledger. + \item A ``Private Key'' is a cryptographic key, the use of which is necessary to modify the information about a Unique Identifier recorded in a Ledger. - \item An ``NFT'' is a Unique Identifier in a Ledger that is Associated with a person or entity when that person or entity has substantially exclusive control over the Private Key necessary to modify the information about that Unique Identifier in that Ledger. + \item An NFT is ``Associated'' with a person or entity when that person or entity has substantially exclusive control over the Private Key necessary to modify the information about that Unique Identifier in that Ledger. - \item The ``Controller'' of an NFT is the person or entity with whom the NFT is Associated. + \item The ``Controller'' of an NFT is the person or entity with whom the NFT is Associated. + + \item An ``NFT Generator'' is a smart contract, function, or other service that creates NFTs in a Ledger. + \end{itemize} \subsect{Licensing} @@ -257,28 +298,39 @@ \section{License Text} \item An NFT is ``Sublicensed'' when the NFT indicates that it has been sublicensed by means of a Licensing Process. + \item \iflicenseoption{\derivativetracking}{Adapted Material is ``Derivative Tracked'' from an NFT (the ``Parent NFT'') when the Parent NFT is Sublicensed by means of a Licensing Process that (1) creates another NFT (the ``Child NFT'') by means of the same NFT Generator that created the Parent NFT, (2) causes the Child NFT to be Linked to Adapted Material, and (3) causes the Child NFT and Parent NFT to have materially the same properties and functionality, except for the identity of the parties they are Associated with and the identity of the material they are Linked to.} + + \item \iflicenseoption{\sharealike}{Adapted Material is ``Share-Alike Sublicensed'' from an NFT (the ``Parent NFT'') when the Parent NFT is Sublicensed by means of a Licensing Process that (1) creates another NFT (the ``Child NFT'') by means of the same NFT Generator that created the Parent NFT, (2) causes the Child NFT to be Linked to Adapted Material, (3) causes the Child NFT and Parent NFT to have materially the same properties and functionality, except for the identity of the parties they are Associated with and the identity of the material they are Linked to, and (4)causes the Child NFT to Invoke this license.} + \item To ``Transfer'' an NFT is to change, modify, or update the Ledger such that the identity of the Owner of that NFT changes, by any legally sufficient means, including sale, barter, gift, bequest, or operation of law. + + \item ``Grace Period Process'' means an interface, function, method, or similar technical process that is part of a Licensing Process and which, when invoked, indicates whether a specified limited duration following the Transfer of an NFT has elapsed. + + \item ``Grace Period'' means the time following the Transfer of an NFT during which the Grace Period Process indicates that the specified limited duration has not yet elapsed. \end{itemize} +\sect{Public License Grant} + +\iflicenseoption{\publiclicense}{The Licensed Material is made available to the public under the terms of the Creative Commons Attribution-NoDerivatives 4.0 International Public License at \href{https://creativecommons.org/licenses/by-nd/4.0/legalcode}{https://creativecommons.org/licenses/by-nd/4.0/legalcode}.} -\sect{License Grant} +\sect{NFT License Grant} -Subject to the terms and conditions of this License, and for as long as You are the Owner of the License NFT, the Licensor hereby grants to You a worldwide, royalty-free, sublicensable, non-exclusive, license to exercise the Licensed Rights to: +Subject to the terms and conditions of this License, and on the condition that You are the Owner of the License NFT, the Licensor hereby grants to You a worldwide, royalty-free, sublicensable, non-exclusive, license to exercise the Licensed Rights to: \begin{itemize} \item Use the Licensed Material \iflicenseoption{\noncommercial}{for Non-Commercial purposes only}, -\item \iflicenseoption{\derivative}{Create and Use Adapted Material \iflicenseoption{\noncommercial}{for Non-Commercial purposes only},} +\item \ifnotlicenseoption{\noderivative}{Create and Use Adapted Material \iflicenseoption{\noncommercial}{for Non-Commercial purposes only} \iflicenseoption{\derivativetracking}{provided that the Adapted Material is Derivative Tracked from the License NFT} \iflicenseoption{\sharealike}{provided that the Adapted Material is Share-Alike Sublicensed from the License NFT}}, \item Identify You as the Owner of the License NFT, \item Use the Licensed Material in connection with the sale and advertising for sale of the License NFT, \end{itemize} -This License becomes effective when the Licensor causes the License NFT to Invoke it. Because this is a unilateral license grant and not a contract, Your assent is not required for it to become effective. +This License becomes effective when the Licensor causes the License NFT to Invoke it. Because this is a unilateral license grant and not a contract, Your assent is not required for it to become effective. \iflicenseoption{\publiclicense}{This license grant is in addition to any rights granted to You as a member of the public under the terms of the Public License Grant above.} All rights granted under this License last for the full duration of the Licensor's Licensed Rights, except that if the License NFT Revokes this License, Your rights under this license grant will terminate, along with all sublicenses granted hereunder. -This license grant is non-transferable, except that if the License NFT is Transferred, Your rights under this license grant will terminate, and such rights will automatically transfer to the new Owner of the License NFT. Any sublicenses granted hereunder that are Sublicensed will continue in force as sublicenses from the new Owner. If You have become the Owner of the License NFT as a result of a Transfer, you automatically grant any such sublicenses that are Sublicensed. This License does not by itself require you to continue or grant any sublicenses given by a previous Owner that were not Sublicensed, but it does not prohibit other law from doing so. - +This license grant is non-transferable. If the License NFT is Transferred such that You are no longer the Owner, Your rights under this license grant will terminate. Notwithstanding the previous sentence, if the Licensing Process contains a Grace Period Process, You may continue to Use the Licensed Material and any already-existing Adapted Material under the terms of the license grant above during the Grace Period, but You may not Create or Use new Adapted Material or grant new sublicenses during the Grace Period. +If the License NFT is Transferred, any sublicenses granted hereunder that are Sublicensed will continue in force as sublicenses from the new Owner. If You have become the Owner of the License NFT as a result of a Transfer, you automatically grant any such sublicenses that are Sublicensed. This License does not by itself require you to continue or grant any sublicenses given by a previous Owner that were not Sublicensed, but it does not prohibit other law from doing so. \sect{Interpretation} diff --git a/assets/eip-5606/contracts/MultiverseNFT.sol b/assets/eip-5606/contracts/MultiverseNFT.sol index d49009e162de80..b2506198be5ba2 100644 --- a/assets/eip-5606/contracts/MultiverseNFT.sol +++ b/assets/eip-5606/contracts/MultiverseNFT.sol @@ -75,8 +75,6 @@ interface IMultiverseNFT { address contractAddress; uint256 tokenId; uint256 quantity; - bool isBundled; - address ownerAddress; } /** @@ -118,8 +116,7 @@ interface IMultiverseNFT { */ function bundle( DelegateData[] memory delegateData, - uint256 multiverseTokenID, - address ownerAddress + uint256 multiverseTokenID ) external; /** @@ -142,11 +139,6 @@ abstract contract MultiverseNFT is uint256 currentMultiverseTokenID; - struct RemainingQuantity { - uint256 multiverseTokenID; - address contractAddress; - } - mapping(uint256 => DelegateData[]) public multiverseNFTDelegateData; mapping(uint256 => mapping(address => mapping(uint256 => uint256))) public tokenBalances; @@ -185,19 +177,14 @@ abstract contract MultiverseNFT is function bundle( DelegateData[] memory delegateData, - uint256 multiverseTokenID, - address ownerAddress + uint256 multiverseTokenID ) external { require( hasRole(BUNDLER_ROLE, msg.sender) || ownerOf(multiverseTokenID) == msg.sender, "msg.sender neither have bundler role nor multiversetoken owner" ); - require( - ownerOf(multiverseTokenID) == ownerAddress, - "ownerAddress is not an owner of multiversetoken" - ); - _bundle(delegateData, multiverseTokenID, ownerAddress); + _bundle(delegateData, multiverseTokenID); } function unbundle( @@ -223,10 +210,6 @@ abstract contract MultiverseNFT is delegateData[i].quantity <= balance, "quantity exceeds balance" ); - require( - _ensureDelegateCanUnbundled(delegateData[i], multiverseTokenID), - "delegate cannot be unbundled" - ); require( _ensureMultiverseContractOwnsDelegate(delegateData[i]), "delegate not owned by contract" @@ -236,16 +219,7 @@ abstract contract MultiverseNFT is uint256 tokenId = delegateData[i].tokenId; uint256 quantity = delegateData[i].quantity; - uint256 remainingBalance = _updateDelegateBalances( - delegateData[i], - multiverseTokenID - ); - if (remainingBalance == 0) { - _updateDelegateStatusToUnbundled( - delegateData[i], - multiverseTokenID - ); - } + _updateDelegateBalances(delegateData[i], multiverseTokenID); if (_isERC721(contractAddress)) { ERC721Full erc721Instance = ERC721Full(contractAddress); @@ -259,8 +233,6 @@ abstract contract MultiverseNFT is quantity, "" ); - } else { - revert("unable to identify ERC std"); } } emit Unbundled(multiverseTokenID, delegateData); @@ -279,8 +251,7 @@ abstract contract MultiverseNFT is function _bundle( DelegateData[] memory delegateData, - uint256 multiverseTokenID, - address ownerAddress + uint256 multiverseTokenID ) internal { for (uint256 i = 0; i < delegateData.length; i = i.add(1)) { require( @@ -308,13 +279,6 @@ abstract contract MultiverseNFT is quantity ); - _updateDelegateStatusAndOwner( - delegateData[i], - ownerAddress, - true, - multiverseTokenID - ); - if (_isERC721(contractAddress)) { require( quantity == 1, @@ -331,11 +295,13 @@ abstract contract MultiverseNFT is quantity, "" ); - } else { - revert("unable to identify ERC std"); } } - emit Bundled(multiverseTokenID, delegateData, ownerAddress); + emit Bundled( + multiverseTokenID, + delegateData, + ownerOf(multiverseTokenID) + ); } function _ensureDelegateBelongsToMultiverseNFT( @@ -382,26 +348,6 @@ abstract contract MultiverseNFT is return false; } - function _ensureDelegateCanUnbundled( - DelegateData memory delegateData, - uint256 multiverseTokenID - ) internal view returns (bool) { - DelegateData[] memory storedData = multiverseNFTDelegateData[ - multiverseTokenID - ]; - for (uint256 i = 0; i < storedData.length; i = i.add(1)) { - if ( - delegateData.contractAddress == storedData[i].contractAddress && - delegateData.tokenId == storedData[i].tokenId && - storedData[i].isBundled && - msg.sender == storedData[i].ownerAddress - ) { - return true; - } - } - return false; - } - function _ensureDelegateQuantityLimitForMMultiverseNFT( DelegateData memory delegateData, uint256 multiverseTokenID @@ -425,49 +371,6 @@ abstract contract MultiverseNFT is return false; } } - - return false; - } - - function _updateDelegateStatusAndOwner( - DelegateData memory delegateData, - address ownerAddress, - bool status, - uint256 multiverseTokenID - ) internal returns (bool) { - DelegateData[] storage storedData = multiverseNFTDelegateData[ - multiverseTokenID - ]; - for (uint256 i = 0; i < storedData.length; i = i.add(1)) { - if ( - delegateData.contractAddress == storedData[i].contractAddress && - delegateData.tokenId == storedData[i].tokenId - ) { - storedData[i].isBundled = status; - storedData[i].ownerAddress = ownerAddress; - return true; - } - } - return false; - } - - function _updateDelegateStatusToUnbundled( - DelegateData memory delegateData, - uint256 multiverseTokenID - ) internal returns (bool) { - DelegateData[] storage storedData = multiverseNFTDelegateData[ - multiverseTokenID - ]; - for (uint256 i = 0; i < storedData.length; i = i.add(1)) { - if ( - delegateData.contractAddress == storedData[i].contractAddress && - delegateData.tokenId == storedData[i].tokenId - ) { - storedData[i].isBundled = false; - return true; - } - } - return false; } function _updateDelegateBalances( @@ -504,18 +407,6 @@ abstract contract MultiverseNFT is return this.onERC1155BatchReceived.selector; } - function _beforeTokenTransfer( - address from, - address to, - uint256 tokenId - ) internal override(ERC721Full) { - DelegateData[] storage storedData = multiverseNFTDelegateData[tokenId]; - for (uint256 i = 0; i < storedData.length; i = i.add(1)) { - storedData[i].ownerAddress = to; - } - ERC721Full._beforeTokenTransfer(from, to, tokenId); - } - function _isERC1155(address contractAddress) internal view returns (bool) { return IERC1155(contractAddress).supportsInterface(0xd9b67a26); } diff --git a/assets/eip-5727/.gitignore b/assets/eip-5727/.gitignore new file mode 100644 index 00000000000000..45ba613caf3beb --- /dev/null +++ b/assets/eip-5727/.gitignore @@ -0,0 +1,354 @@ +node_modules +.env +coverage +coverage.json +typechain/* +!typechain/package.json +!typechain/tsconfig.json +!typechain/README.md +typechain-types +.deps +.vscode + +#Hardhat files +cache +artifacts + +# Created by https://www.toptal.com/developers/gitignore/api/node +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +# End of https://www.toptal.com/developers/gitignore/api/node + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,solidity +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,macos,solidity + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Solidity ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,solidity \ No newline at end of file diff --git a/assets/eip-5727/contracts/ERC5727.sol b/assets/eip-5727/contracts/ERC5727.sol new file mode 100644 index 00000000000000..d3331759213584 --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727.sol @@ -0,0 +1,317 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/access/AccessControlEnumerable.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; +import "@openzeppelin/contracts/utils/Counters.sol"; + +import "./interfaces/IERC5727.sol"; +import "./interfaces/IERC5727Metadata.sol"; + +abstract contract ERC5727 is + IERC5727Metadata, + ERC165, + Ownable, + AccessControlEnumerable +{ + using Address for address; + using Strings for uint256; + + struct Token { + address issuer; + address soul; + bool valid; + uint256 value; + uint256 slot; + } + + mapping(uint256 => Token) private _tokens; + + string private _name; + string private _symbol; + + constructor(string memory name_, string memory symbol_) Ownable() { + _name = name_; + _symbol = symbol_; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC165, IERC165, AccessControlEnumerable) + returns (bool) + { + return + interfaceId == type(IERC5727).interfaceId || + interfaceId == type(IERC5727Metadata).interfaceId || + super.supportsInterface(interfaceId); + } + + function _getTokenOrRevert(uint256 tokenId) + internal + view + virtual + returns (Token storage) + { + Token storage token = _tokens[tokenId]; + require(token.soul != address(0), "ERC5727: Token does not exist"); + return token; + } + + function _mintUnsafe( + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal { + _mintUnsafe(_msgSender(), soul, tokenId, value, slot, valid); + } + + function _mintUnsafe( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal { + require( + _tokens[tokenId].soul == address(0), + "ERC5727: Cannot mint an assigned token" + ); + require(value != 0, "ERC5727: Cannot mint zero value"); + _beforeTokenMint(issuer, soul, tokenId, value, slot, valid); + _tokens[tokenId] = Token(issuer, soul, valid, value, slot); + _afterTokenMint(issuer, soul, tokenId, value, slot, valid); + emit Minted(soul, tokenId, value); + emit SlotChanged(tokenId, 0, slot); + } + + function _charge(uint256 tokenId, uint256 value) internal virtual { + _getTokenOrRevert(tokenId).value += value; + emit Charged(tokenId, value); + } + + function _chargeBatch(uint256[] memory tokenIds, uint256[] memory values) + internal + virtual + { + require( + tokenIds.length == values.length, + "ERC5727: unmatched size of tokenIds and values" + ); + for (uint256 i = 0; i < tokenIds.length; i++) { + _charge(tokenIds[i], values[i]); + } + } + + function _consume(uint256 tokenId, uint256 value) internal virtual { + require( + _getTokenOrRevert(tokenId).value > value, + "ERC5727: not enough balance" + ); + _getTokenOrRevert(tokenId).value -= value; + emit Consumed(tokenId, value); + } + + function _consumeBatch(uint256[] memory tokenIds, uint256[] memory values) + internal + virtual + { + require( + tokenIds.length == values.length, + "ERC5727: unmatched size of tokenIds and values" + ); + for (uint256 i = 0; i < tokenIds.length; i++) { + _consume(tokenIds[i], values[i]); + } + } + + function _revoke(uint256 tokenId) internal virtual { + require( + _getTokenOrRevert(tokenId).valid, + "ERC5727: Token is already invalid" + ); + _beforeTokenRevoke(tokenId); + _tokens[tokenId].valid = false; + _afterTokenRevoke(tokenId); + emit Revoked(_tokens[tokenId].soul, tokenId); + } + + function _revokeBatch(uint256[] memory tokenIds) internal virtual { + for (uint256 i = 0; i < tokenIds.length; i++) { + _revoke(tokenIds[i]); + } + } + + function _destroy(uint256 tokenId) internal virtual { + address soul = soulOf(tokenId); + uint256 slot = slotOf(tokenId); + uint256 value = valueOf(tokenId); + + _beforeTokenDestroy(tokenId); + delete _tokens[tokenId]; + _afterTokenDestroy(tokenId); + + emit Consumed(tokenId, value); + emit Destroyed(soul, tokenId); + emit SlotChanged(tokenId, slot, 0); + } + + function _isCreator() internal view virtual returns (bool) { + return _msgSender() == owner(); + } + + function _beforeView(uint256 tokenId) internal view virtual {} + + function _beforeTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual {} + + function _afterTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual {} + + function _beforeTokenRevoke(uint256 tokenId) internal virtual {} + + function _afterTokenRevoke(uint256 tokenId) internal virtual {} + + function _beforeTokenDestroy(uint256 tokenId) internal virtual {} + + function _afterTokenDestroy(uint256 tokenId) internal virtual {} + + function soulOf(uint256 tokenId) + public + view + virtual + override + returns (address) + { + _beforeView(tokenId); + return _getTokenOrRevert(tokenId).soul; + } + + function valueOf(uint256 tokenId) + public + view + virtual + override + returns (uint256) + { + _beforeView(tokenId); + return _getTokenOrRevert(tokenId).value; + } + + function slotOf(uint256 tokenId) + public + view + virtual + override + returns (uint256) + { + _beforeView(tokenId); + return _getTokenOrRevert(tokenId).slot; + } + + function issuerOf(uint256 tokenId) + public + view + virtual + override + returns (address) + { + return _getTokenOrRevert(tokenId).issuer; + } + + function isValid(uint256 tokenId) + public + view + virtual + override + returns (bool) + { + _beforeView(tokenId); + return _getTokenOrRevert(tokenId).valid; + } + + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + function contractURI() + public + view + virtual + override + returns (string memory) + { + string memory baseURI = _baseURI(); + return + bytes(baseURI).length > 0 + ? string( + abi.encodePacked( + baseURI, + "contract/", + Strings.toHexString(uint256(uint160(address(this)))) + ) + ) + : ""; + } + + function slotURI(uint256 slot) + public + view + virtual + override + returns (string memory) + { + string memory baseURI = _baseURI(); + return + bytes(baseURI).length > 0 + ? string(abi.encodePacked(baseURI, "slot/", slot.toString())) + : ""; + } + + function tokenURI(uint256 tokenId) + public + view + virtual + override + returns (string memory) + { + _getTokenOrRevert(tokenId); + bytes memory baseURI = bytes(_baseURI()); + if (baseURI.length > 0) { + return + string( + abi.encodePacked(baseURI, Strings.toHexString(tokenId, 32)) + ); + } + return ""; + } + + function name() public view virtual override returns (string memory) { + return _name; + } + + function symbol() public view virtual override returns (string memory) { + return _symbol; + } +} diff --git a/assets/eip-5727/contracts/ERC5727Delegate.sol b/assets/eip-5727/contracts/ERC5727Delegate.sol new file mode 100644 index 00000000000000..36cbe90e5e3816 --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Delegate.sol @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Delegate.sol"; +import "./ERC5727Enumerable.sol"; + +abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { + struct DelegateRequest { + address soul; + uint256 value; + uint256 slot; + } + + using EnumerableSet for EnumerableSet.UintSet; + + uint256 private _delegateRequestCount; + + mapping(uint256 => DelegateRequest) private _delegateRequests; + + mapping(address => mapping(uint256 => bool)) private _mintAllowed; + + mapping(address => mapping(uint256 => bool)) private _revokeAllowed; + + mapping(address => EnumerableSet.UintSet) private _delegatedRequests; + + mapping(address => EnumerableSet.UintSet) private _delegatedTokens; + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727Enumerable) + returns (bool) + { + return + interfaceId == type(IERC5727Delegate).interfaceId || + super.supportsInterface(interfaceId); + } + + function _mintDelegateAsDelegateOrCreator( + address operator, + uint256 delegateRequestId, + bool isCreator + ) private { + require( + isCreator || _mintAllowed[_msgSender()][delegateRequestId], + "ERC5727Delegate: Only contract creator or allowed operator can delegate" + ); + if (!isCreator) { + _mintAllowed[_msgSender()][delegateRequestId] = false; + _delegatedRequests[_msgSender()].remove(delegateRequestId); + } + _mintAllowed[operator][delegateRequestId] = true; + _delegatedRequests[operator].add(delegateRequestId); + } + + function _revokeDelegateAsDelegateOrCreator( + address operator, + uint256 tokenId, + bool isCreator + ) private { + require( + isCreator || _revokeAllowed[_msgSender()][tokenId], + "ERC5727Delegate: Only contract creator or allowed operator can delegate" + ); + if (!isCreator) { + _revokeAllowed[_msgSender()][tokenId] = false; + _delegatedTokens[_msgSender()].remove(tokenId); + } + _revokeAllowed[operator][tokenId] = true; + _delegatedTokens[operator].add(tokenId); + } + + function _mintAsDelegateOrCreator(uint256 delegateRequestId, bool isCreator) + private + { + require( + isCreator || _mintAllowed[_msgSender()][delegateRequestId], + "ERC5727Delegate: Only contract creator or allowed operator can mint" + ); + if (!isCreator) { + _mintAllowed[_msgSender()][delegateRequestId] = false; + } + _mint( + _delegateRequests[delegateRequestId].soul, + _delegateRequests[delegateRequestId].value, + _delegateRequests[delegateRequestId].slot + ); + } + + function _revokeAsDelegateOrCreator(uint256 tokenId, bool isCreator) + private + { + require( + isCreator || _revokeAllowed[_msgSender()][tokenId], + "ERC5727Delegate: Only contract creator or allowed operator can revoke" + ); + if (!isCreator) { + _revokeAllowed[_msgSender()][tokenId] = false; + } + _revoke(tokenId); + } + + function mintDelegate(address operator, uint256 delegateRequestId) + public + virtual + override + { + _mintDelegateAsDelegateOrCreator( + operator, + delegateRequestId, + _isCreator() + ); + } + + function mintDelegateBatch( + address[] memory operators, + uint256[] memory delegateRequestIds + ) public virtual override { + require( + operators.length == delegateRequestIds.length, + "ERC5727Delegate: operators and delegateRequestIds must have the same length" + ); + bool isCreator = _isCreator(); + for (uint256 i = 0; i < operators.length; i++) { + _mintDelegateAsDelegateOrCreator( + operators[i], + delegateRequestIds[i], + isCreator + ); + } + } + + function revokeDelegate(address operator, uint256 tokenId) + public + virtual + override + { + _revokeDelegateAsDelegateOrCreator(operator, tokenId, _isCreator()); + } + + function revokeDelegateBatch( + address[] memory operators, + uint256[] memory tokenIds + ) public virtual override { + require( + operators.length == tokenIds.length, + "ERC5727Delegate: operators and tokenIds must have the same length" + ); + bool isCreator = _isCreator(); + for (uint256 i = 0; i < operators.length; i++) { + _revokeDelegateAsDelegateOrCreator( + operators[i], + tokenIds[i], + isCreator + ); + } + } + + function delegateMint(uint256 delegateRequestId) public virtual override { + _mintAsDelegateOrCreator(delegateRequestId, _isCreator()); + } + + function delegateMintBatch(uint256[] memory delegateRequestIds) + public + virtual + override + { + bool isCreator = _isCreator(); + for (uint256 i = 0; i < delegateRequestIds.length; i++) { + _mintAsDelegateOrCreator(delegateRequestIds[i], isCreator); + } + } + + function delegateRevoke(uint256 tokenId) public virtual override { + _revokeAsDelegateOrCreator(tokenId, _isCreator()); + } + + function delegateRevokeBatch(uint256[] memory tokenIds) + public + virtual + override + { + bool isCreator = _isCreator(); + for (uint256 i = 0; i < tokenIds.length; i++) { + _revokeAsDelegateOrCreator(tokenIds[i], isCreator); + } + } + + function createDelegateRequest( + address soul, + uint256 value, + uint256 slot + ) external virtual override returns (uint256 delegateRequestId) { + require(_isCreator(), "ERC5727Delegate: You are not the creator"); + require( + value != 0, + "ERC5727Delegate: Value of Delegate Request cannot be zero" + ); + delegateRequestId = _delegateRequestCount; + _delegateRequests[_delegateRequestCount].soul = soul; + _delegateRequests[_delegateRequestCount].value = value; + _delegateRequests[_delegateRequestCount].slot = slot; + _delegateRequestCount++; + } + + function removeDelegateRequest(uint256 delegateRequestId) + external + virtual + override + { + require(_isCreator(), "ERC5727Delegate: You are not the creator"); + delete _delegateRequests[delegateRequestId]; + } + + function delegatedRequestsOf(address operator) + public + view + virtual + returns (uint256[] memory) + { + return _delegatedRequests[operator].values(); + } + + function delegatedTokensOf(address operator) + public + view + virtual + returns (uint256[] memory) + { + return _delegatedTokens[operator].values(); + } + + function soulOfDelegateRequest(uint256 delegateRequestId) + public + view + virtual + returns (address) + { + require( + delegateRequestId < _delegateRequestCount, + "ERC5727Delegate: Delegate request does not exist" + ); + return _delegateRequests[delegateRequestId].soul; + } + + function valueOfDelegateRequest(uint256 delegateRequestId) + public + view + virtual + returns (uint256) + { + require( + delegateRequestId < _delegateRequestCount, + "ERC5727Delegate: Delegate request does not exist" + ); + return _delegateRequests[delegateRequestId].value; + } + + function slotOfDelegateRequest(uint256 delegateRequestId) + public + view + virtual + returns (uint256) + { + require( + delegateRequestId < _delegateRequestCount, + "ERC5727Delegate: Delegate request does not exist" + ); + return _delegateRequests[delegateRequestId].slot; + } +} diff --git a/assets/eip-5727/contracts/ERC5727Enumerable.sol b/assets/eip-5727/contracts/ERC5727Enumerable.sol new file mode 100644 index 00000000000000..9d76812451b159 --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Enumerable.sol @@ -0,0 +1,188 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/Counters.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Enumerable.sol"; + +abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { + using Counters for Counters.Counter; + using EnumerableSet for EnumerableSet.UintSet; + + mapping(address => EnumerableSet.UintSet) private _indexedTokenIds; + mapping(address => uint256) private _numberOfValidTokens; + + Counters.Counter private _emittedCount; + Counters.Counter private _soulsCount; + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727) + returns (bool) + { + return + interfaceId == type(IERC5727Enumerable).interfaceId || + super.supportsInterface(interfaceId); + } + + function _beforeTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual override { + if (_indexedTokenIds[soul].length() == 0) { + _soulsCount.increment(); + } + //unused variables + issuer; + tokenId; + value; + slot; + valid; + } + + function _afterTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual override { + _indexedTokenIds[soul].add(tokenId); + if (valid) { + _numberOfValidTokens[soul] += 1; + } + //unused variables + issuer; + value; + slot; + valid; + } + + function _mint( + address soul, + uint256 value, + uint256 slot + ) internal virtual returns (uint256 tokenId) { + tokenId = _emittedCount.current(); + _mintUnsafe(soul, tokenId, value, slot, true); + emit Minted(soul, tokenId, value); + _emittedCount.increment(); + } + + function _mint( + address issuer, + address soul, + uint256 value, + uint256 slot + ) internal virtual returns (uint256 tokenId) { + tokenId = _emittedCount.current(); + _mintUnsafe(issuer, soul, tokenId, value, slot, true); + emit Minted(soul, tokenId, value); + _emittedCount.increment(); + } + + function _mintBatch( + address[] memory souls, + uint256 value, + uint256 slot + ) internal virtual returns (uint256[] memory tokenIds) { + tokenIds = new uint256[](souls.length); + for (uint256 i = 0; i < souls.length; i++) { + tokenIds[i] = _mint(souls[i], value, slot); + } + } + + function _afterTokenRevoke(uint256 tokenId) internal virtual override { + assert(_numberOfValidTokens[_getTokenOrRevert(tokenId).soul] > 0); + _numberOfValidTokens[_getTokenOrRevert(tokenId).soul] -= 1; + } + + function _beforeTokenDestroy(uint256 tokenId) internal virtual override { + address soul = soulOf(tokenId); + + if (_getTokenOrRevert(tokenId).valid) { + assert(_numberOfValidTokens[soul] > 0); + _numberOfValidTokens[soul] -= 1; + } + EnumerableSet.remove(_indexedTokenIds[soul], tokenId); + if (EnumerableSet.length(_indexedTokenIds[soul]) == 0) { + assert(_soulsCount.current() > 0); + _soulsCount.decrement(); + } + } + + function _increaseEmittedCount() internal { + _emittedCount.increment(); + } + + function _tokensOfSoul(address soul) + internal + view + returns (uint256[] memory tokenIds) + { + tokenIds = _indexedTokenIds[soul].values(); + require(tokenIds.length != 0, "ERC5727: the soul has no token"); + } + + function emittedCount() public view virtual override returns (uint256) { + return _emittedCount.current(); + } + + function soulsCount() public view virtual override returns (uint256) { + return _soulsCount.current(); + } + + function balanceOf(address soul) + public + view + virtual + override + returns (uint256) + { + return _indexedTokenIds[soul].length(); + } + + function hasValid(address soul) + public + view + virtual + override + returns (bool) + { + return _numberOfValidTokens[soul] > 0; + } + + function tokenOfSoulByIndex(address soul, uint256 index) + public + view + virtual + override + returns (uint256) + { + EnumerableSet.UintSet storage ids = _indexedTokenIds[soul]; + require( + index < EnumerableSet.length(ids), + "ERC5727: Token does not exist" + ); + return ids.at(index); + } + + function tokenByIndex(uint256 index) + public + view + virtual + override + returns (uint256) + { + return index; + } +} diff --git a/assets/eip-5727/contracts/ERC5727Example.sol b/assets/eip-5727/contracts/ERC5727Example.sol new file mode 100644 index 00000000000000..f357b05cd1fbf7 --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Example.sol @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "./ERC5727Expirable.sol"; +import "./ERC5727Governance.sol"; +import "./ERC5727Delegate.sol"; +import "./ERC5727Shadow.sol"; +import "./ERC5727SlotEnumerable.sol"; +import "./ERC5727Recovery.sol"; + +contract ERC5727Example is + ERC5727Expirable, + ERC5727Governance, + ERC5727Delegate, + ERC5727SlotEnumerable, + ERC5727Shadow, + ERC5727Recovery +{ + string private _baseTokenURI; + + constructor( + string memory name, + string memory symbol, + address[] memory voters, + string memory baseTokenURI + ) ERC5727Governance(name, symbol, voters) { + _baseTokenURI = baseTokenURI; + } + + function _baseURI() internal view virtual override returns (string memory) { + return _baseTokenURI; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override( + ERC5727Governance, + ERC5727Delegate, + ERC5727Expirable, + ERC5727Recovery, + ERC5727Shadow, + ERC5727SlotEnumerable + ) + returns (bool) + { + return super.supportsInterface(interfaceId); + } + + function _beforeView(uint256 tokenId) + internal + view + virtual + override(ERC5727, ERC5727Shadow) + { + ERC5727Shadow._beforeView(tokenId); + } + + function mint( + address soul, + uint256 value, + uint256 slot, + uint256 expiryDate, + bool shadowed + ) public virtual onlyOwner { + uint256 tokenId = _mint(soul, value, slot); + if (shadowed) { + _shadow(tokenId); + } + _setExpiryDate(tokenId, expiryDate); + } + + function revoke(uint256 tokenId) public virtual onlyOwner { + _revoke(tokenId); + } + + function mintBatch( + address[] memory souls, + uint256 value, + uint256 slot, + uint256 expiryDate, + bool shadowed + ) public virtual onlyOwner { + uint256[] memory tokenIds = _mintBatch(souls, value, slot); + for (uint256 i = 0; i < tokenIds.length; i++) { + if (shadowed) _shadow(tokenIds[i]); + _setExpiryDate(tokenIds[i], expiryDate); + } + } + + function revokeBatch(uint256[] memory tokenIds) public virtual onlyOwner { + _revokeBatch(tokenIds); + } + + function _beforeTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) + internal + virtual + override(ERC5727, ERC5727Enumerable, ERC5727SlotEnumerable) + { + ERC5727Enumerable._beforeTokenMint( + issuer, + soul, + tokenId, + value, + slot, + valid + ); + ERC5727SlotEnumerable._beforeTokenMint( + issuer, + soul, + tokenId, + value, + slot, + valid + ); + } + + function _afterTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual override(ERC5727, ERC5727Enumerable) { + ERC5727Enumerable._afterTokenMint( + issuer, + soul, + tokenId, + value, + slot, + valid + ); + } + + function _afterTokenRevoke(uint256 tokenId) + internal + virtual + override(ERC5727, ERC5727Enumerable) + { + ERC5727Enumerable._afterTokenRevoke(tokenId); + } + + function _beforeTokenDestroy(uint256 tokenId) + internal + virtual + override(ERC5727, ERC5727Enumerable, ERC5727SlotEnumerable) + { + ERC5727Enumerable._beforeTokenDestroy(tokenId); + ERC5727SlotEnumerable._beforeTokenDestroy(tokenId); + } +} diff --git a/assets/eip-5727/contracts/ERC5727Expirable.sol b/assets/eip-5727/contracts/ERC5727Expirable.sol new file mode 100644 index 00000000000000..822ed927972a7d --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Expirable.sol @@ -0,0 +1,93 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Expirable.sol"; + +abstract contract ERC5727Expirable is IERC5727Expirable, ERC5727 { + mapping(uint256 => uint256) private _expiryDate; + + function expiryDate(uint256 tokenId) + public + view + virtual + override + returns (uint256) + { + uint256 date = _expiryDate[tokenId]; + require(date != 0, "ERC5727Expirable: No expiry date set"); + return date; + } + + function isExpired(uint256 tokenId) + public + view + virtual + override + returns (bool) + { + uint256 date = _expiryDate[tokenId]; + require(date != 0, "ERC5727Expirable: No expiry date set"); + return date < block.timestamp; + } + + function _setExpiryDate(uint256 tokenId, uint256 date) + internal + virtual + onlyOwner + { + require( + date > block.timestamp, + "ERC5727Expirable: Expiry date cannot be in the past" + ); + require( + date > _expiryDate[tokenId], + "ERC5727Expirable: Expiry date can only be extended" + ); + _expiryDate[tokenId] = date; + } + + function _setBatchExpiryDates( + uint256[] memory tokenIds, + uint256[] memory dates + ) internal { + require( + tokenIds.length == dates.length, + "ERC5727Expirable: Ids and token URIs length mismatch" + ); + for (uint256 i = 0; i < tokenIds.length; i++) { + _setExpiryDate(tokenIds[i], dates[i]); + } + } + + function _setBatchExpiryDates(uint256[] memory tokenIds, uint256 date) + internal + { + for (uint256 i = 0; i < tokenIds.length; i++) { + _setExpiryDate(tokenIds[i], date); + } + } + + function setExpiryDate(uint256 tokenId, uint256 date) public onlyOwner { + _setExpiryDate(tokenId, date); + } + + function setBatchExpiryDates( + uint256[] memory tokenIds, + uint256[] memory dates + ) public onlyOwner { + _setBatchExpiryDates(tokenIds, dates); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727) + returns (bool) + { + return + interfaceId == type(IERC5727Expirable).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5727/contracts/ERC5727Governance.sol b/assets/eip-5727/contracts/ERC5727Governance.sol new file mode 100644 index 00000000000000..3c10bbe1ce9726 --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Governance.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Governance.sol"; +import "./ERC5727Enumerable.sol"; + +abstract contract ERC5727Governance is ERC5727Enumerable, IERC5727Governance { + using EnumerableSet for EnumerableSet.AddressSet; + + uint256 private _approvalRequestCount; + + struct ApprovalRequest { + address creator; + uint256 value; + uint256 slot; + } + + mapping(uint256 => ApprovalRequest) private _approvalRequests; + + EnumerableSet.AddressSet private _votersArray; + + mapping(address => mapping(uint256 => mapping(address => bool))) + private _mintApprovals; + + mapping(uint256 => mapping(address => uint256)) private _mintApprovalCounts; + + mapping(address => mapping(uint256 => bool)) private _revokeApprovals; + + mapping(uint256 => uint256) private _revokeApprovalCounts; + + bytes32 public constant VOTER_ROLE = keccak256("VOTER_ROLE"); + + constructor( + string memory name_, + string memory symbol_, + address[] memory voters_ + ) ERC5727(name_, symbol_) { + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + for (uint256 i = 0; i < voters_.length; i++) { + _votersArray.add(voters_[i]); + _setupRole(VOTER_ROLE, voters_[i]); + } + } + + function voters() public view virtual override returns (address[] memory) { + return _votersArray.values(); + } + + function approveMint(address soul, uint256 approvalRequestId) + public + virtual + override + onlyRole(VOTER_ROLE) + { + require( + !_mintApprovals[_msgSender()][approvalRequestId][soul], + "ERC5727Governance: You already approved this address" + ); + _mintApprovals[_msgSender()][approvalRequestId][soul] = true; + _mintApprovalCounts[approvalRequestId][soul] += 1; + if ( + _mintApprovalCounts[approvalRequestId][soul] == + _votersArray.length() + ) { + _resetMintApprovals(approvalRequestId, soul); + _mint( + _approvalRequests[approvalRequestId].creator, + soul, + _approvalRequests[approvalRequestId].value, + _approvalRequests[approvalRequestId].slot + ); + } + } + + function approveRevoke(uint256 tokenId) + public + virtual + override + onlyRole(VOTER_ROLE) + { + require( + !_revokeApprovals[_msgSender()][tokenId], + "ERC5727Governance: You already approved this address" + ); + _revokeApprovals[_msgSender()][tokenId] = true; + _revokeApprovalCounts[tokenId] += 1; + if (_revokeApprovalCounts[tokenId] == _votersArray.length()) { + _resetRevokeApprovals(tokenId); + _revoke(tokenId); + } + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727Enumerable) + returns (bool) + { + return + interfaceId == type(IERC5727Governance).interfaceId || + super.supportsInterface(interfaceId); + } + + function _resetMintApprovals(uint256 approvalRequestId, address soul) + private + { + for (uint256 i = 0; i < _votersArray.length(); i++) { + _mintApprovals[_votersArray.at(i)][approvalRequestId][soul] = false; + } + _mintApprovalCounts[approvalRequestId][soul] = 0; + } + + function _resetRevokeApprovals(uint256 tokenId) private { + for (uint256 i = 0; i < _votersArray.length(); i++) { + _revokeApprovals[_votersArray.at(i)][tokenId] = false; + } + _revokeApprovalCounts[tokenId] = 0; + } + + function createApprovalRequest(uint256 value, uint256 slot) + external + virtual + override + { + require( + value != 0, + "ERC5727Governance: Value of Approval Request cannot be 0" + ); + _approvalRequests[_approvalRequestCount] = ApprovalRequest( + _msgSender(), + value, + slot + ); + _approvalRequestCount++; + } + + function removeApprovalRequest(uint256 approvalRequestId) + external + virtual + override + { + require( + _msgSender() == _approvalRequests[approvalRequestId].creator, + "ERC5727Governance: You are not the creator" + ); + delete _approvalRequests[approvalRequestId]; + } + + function addVoter(address newVoter) public onlyOwner { + require( + !hasRole(VOTER_ROLE, newVoter), + "ERC5727Governance: newVoter is already a voter" + ); + _votersArray.add(newVoter); + _setupRole(VOTER_ROLE, newVoter); + } + + function removeVoter(address voter) public onlyOwner { + require( + _votersArray.contains(voter), + "ERC5727Governance: Voter does not exist" + ); + _revokeRole(VOTER_ROLE, voter); + _votersArray.remove(voter); + } +} diff --git a/assets/eip-5727/contracts/ERC5727Recovery.sol b/assets/eip-5727/contracts/ERC5727Recovery.sol new file mode 100644 index 00000000000000..bf679ce63f36cd --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Recovery.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Recovery.sol"; +import "./ERC5727Enumerable.sol"; + +abstract contract ERC5727Recovery is ERC5727Enumerable, IERC5727Recovery { + using ECDSA for bytes32; + + function recover(address soul, bytes memory signature) + public + virtual + override + { + address recipient = _msgSender(); + bytes32 messageHash = keccak256(abi.encodePacked(soul, recipient)); + bytes32 signedHash = messageHash.toEthSignedMessageHash(); + require(signedHash.recover(signature) == soul, "Invalid signature"); + uint256[] memory tokenIds = _tokensOfSoul(soul); + for (uint256 i = 0; i < tokenIds.length; i++) { + Token storage token = _getTokenOrRevert(tokenIds[i]); + address issuer = token.issuer; + uint256 value = token.value; + uint256 slot = token.slot; + bool valid = token.valid; + _destroy(tokenIds[i]); + _mintUnsafe(issuer, recipient, tokenIds[i], value, slot, valid); + } + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727Enumerable) + returns (bool) + { + return + interfaceId == type(IERC5727Recovery).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5727/contracts/ERC5727Shadow.sol b/assets/eip-5727/contracts/ERC5727Shadow.sol new file mode 100644 index 00000000000000..176d9bd485063a --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727Shadow.sol @@ -0,0 +1,84 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727Shadow.sol"; + +abstract contract ERC5727Shadow is ERC5727, IERC5727Shadow { + mapping(uint256 => bool) private _shadowed; + + modifier onlyManager(uint256 tokenId) { + require( + _msgSender() == _getTokenOrRevert(tokenId).soul || + _msgSender() == _getTokenOrRevert(tokenId).issuer, + "ERC5727Shadow: You are not the manager" + ); + _; + } + + function _beforeView(uint256 tokenId) internal view virtual override { + require( + !_shadowed[tokenId] || + _msgSender() == _getTokenOrRevert(tokenId).soul || + _msgSender() == _getTokenOrRevert(tokenId).issuer, + "ERC5727Shadow: the token is shadowed" + ); + } + + function _shadow(uint256 tokenId) internal virtual { + _getTokenOrRevert(tokenId); + _shadowed[tokenId] = true; + } + + function _unshadow(uint256 tokenId) internal virtual { + _getTokenOrRevert(tokenId); + _shadowed[tokenId] = false; + } + + function _isShadowed(uint256 tokenId) internal view virtual returns (bool) { + _getTokenOrRevert(tokenId); + return _shadowed[tokenId]; + } + + function isShadowed(uint256 tokenId) + public + view + virtual + override + onlyManager(tokenId) + returns (bool) + { + _getTokenOrRevert(tokenId); + return _shadowed[tokenId]; + } + + function shadow(uint256 tokenId) + public + virtual + override + onlyManager(tokenId) + { + _shadowed[tokenId] = true; + } + + function reveal(uint256 tokenId) + public + virtual + override + onlyManager(tokenId) + { + _shadowed[tokenId] = false; + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727) + returns (bool) + { + return + interfaceId == type(IERC5727Shadow).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol b/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol new file mode 100644 index 00000000000000..6dc36723cc60db --- /dev/null +++ b/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol @@ -0,0 +1,95 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +import "./ERC5727.sol"; +import "./interfaces/IERC5727SlotEnumerable.sol"; + +abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { + using EnumerableSet for EnumerableSet.UintSet; + + mapping(uint256 => EnumerableSet.UintSet) private _tokensInSlot; + + EnumerableSet.UintSet private _allSlots; + + function slotCount() public view override returns (uint256) { + return _allSlots.length(); + } + + function slotByIndex(uint256 index) public view override returns (uint256) { + require( + index < ERC5727SlotEnumerable.slotCount(), + "ERC5727SlotEnumerable: slot index out of bounds" + ); + return _allSlots.at(index); + } + + function _slotExists(uint256 slot) internal view virtual returns (bool) { + return _allSlots.length() != 0 && _allSlots.contains(slot); + } + + function tokenSupplyInSlot(uint256 slot) + public + view + override + returns (uint256) + { + if (!_slotExists(slot)) { + return 0; + } + return _tokensInSlot[slot].length(); + } + + function tokenInSlotByIndex(uint256 slot, uint256 index) + public + view + override + returns (uint256) + { + require( + index < ERC5727SlotEnumerable.tokenSupplyInSlot(slot), + "ERC5727SlotEnumerable: slot token index out of bounds" + ); + return _tokensInSlot[slot].at(index); + } + + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC5727) + returns (bool) + { + return + interfaceId == type(IERC5727SlotEnumerable).interfaceId || + super.supportsInterface(interfaceId); + } + + function _beforeTokenMint( + address issuer, + address soul, + uint256 tokenId, + uint256 value, + uint256 slot, + bool valid + ) internal virtual override { + if (!_slotExists(slot)) { + _allSlots.add(slot); + } + _tokensInSlot[slot].add(tokenId); + //unused + issuer; + soul; + value; + valid; + } + + function _beforeTokenDestroy(uint256 tokenId) internal virtual override { + uint256 slot = _getTokenOrRevert(tokenId).slot; + _tokensInSlot[slot].remove(tokenId); + if (_tokensInSlot[slot].length() == 0) { + _allSlots.remove(slot); + } + } +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727.sol b/assets/eip-5727/contracts/interfaces/IERC5727.sol new file mode 100644 index 00000000000000..dddfcce32c53e9 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727.sol @@ -0,0 +1,98 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @title ERC5727 Soulbound Token Interface + * @dev The core interface of the ERC5727 standard. + */ +interface IERC5727 is IERC165 { + /** + * @dev MUST emit when a token is minted. + * @param soul The address that the token is minted to + * @param tokenId The token minted + * @param value The value of the token minted + */ + event Minted(address indexed soul, uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is revoked. + * @param soul The owner soul of the revoked token + * @param tokenId The revoked token + */ + event Revoked(address indexed soul, uint256 indexed tokenId); + + /** + * @dev MUST emit when a token is charged. + * @param tokenId The token to charge + * @param value The value to charge + */ + event Charged(uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is consumed. + * @param tokenId The token to consume + * @param value The value to consume + */ + event Consumed(uint256 indexed tokenId, uint256 value); + + /** + * @dev MUST emit when a token is destroyed. + * @param soul The owner soul of the destroyed token + * @param tokenId The token to destroy. + */ + event Destroyed(address indexed soul, uint256 indexed tokenId); + + /** + * @dev MUST emit when the slot of a token is set or changed. + * @param tokenId The token of which slot is set or changed + * @param oldSlot The previous slot of the token + * @param newSlot The updated slot of the token + */ + event SlotChanged( + uint256 indexed tokenId, + uint256 indexed oldSlot, + uint256 indexed newSlot + ); + + /** + * @notice Get the value of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the balance + * @return The value of `tokenId` + */ + function valueOf(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get the slot of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the slot + * @return The slot of `tokenId` + */ + function slotOf(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get the owner soul of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the owner soul + * @return The address of the owner soul of `tokenId` + */ + function soulOf(uint256 tokenId) external view returns (address); + + /** + * @notice Get the validity of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the validity + * @return If the token is valid + */ + function isValid(uint256 tokenId) external view returns (bool); + + /** + * @notice Get the issuer of a token. + * @dev MUST revert if the `tokenId` does not exist + * @param tokenId the token for which to query the issuer + * @return The address of the issuer of `tokenId` + */ + function issuerOf(uint256 tokenId) external view returns (address); +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol b/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol new file mode 100644 index 00000000000000..eeeeecdb9dc59f --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Delegate Interface + * @dev This extension allows delegation of (batch) minting and revocation of tokens to operator(s). + */ +interface IERC5727Delegate is IERC5727 { + /** + * @notice Delegate a one-time minting right to `operator` for `delegateRequestId` delegate request. + * @dev MUST revert if the caller does not have the right to delegate. + * @param operator The soul to which the minting right is delegated + * @param delegateRequestId The delegate request describing the soul, value and slot of the token to mint + */ + function mintDelegate(address operator, uint256 delegateRequestId) external; + + /** + * @notice Delegate one-time minting rights to `operators` for corresponding delegate request in `delegateRequestIds`. + * @dev MUST revert if the caller does not have the right to delegate. + * MUST revert if the length of `operators` and `delegateRequestIds` do not match. + * @param operators The souls to which the minting right is delegated + * @param delegateRequestIds The delegate requests describing the soul, value and slot of the tokens to mint + */ + function mintDelegateBatch( + address[] memory operators, + uint256[] memory delegateRequestIds + ) external; + + /** + * @notice Delegate a one-time revoking right to `operator` for `tokenId` token. + * @dev MUST revert if the caller does not have the right to delegate. + * @param operator The soul to which the revoking right is delegated + * @param tokenId The token to revoke + */ + function revokeDelegate(address operator, uint256 tokenId) external; + + /** + * @notice Delegate one-time minting rights to `operators` for corresponding token in `tokenIds`. + * @dev MUST revert if the caller does not have the right to delegate. + * MUST revert if the length of `operators` and `tokenIds` do not match. + * @param operators The souls to which the revoking right is delegated + * @param tokenIds The tokens to revoke + */ + function revokeDelegateBatch( + address[] memory operators, + uint256[] memory tokenIds + ) external; + + /** + * @notice Mint a token described by `delegateRequestId` delegate request as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param delegateRequestId The delegate requests describing the soul, value and slot of the token to mint. + */ + function delegateMint(uint256 delegateRequestId) external; + + /** + * @notice Mint tokens described by `delegateRequestIds` delegate request as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param delegateRequestIds The delegate requests describing the soul, value and slot of the tokens to mint. + */ + function delegateMintBatch(uint256[] memory delegateRequestIds) external; + + /** + * @notice Revoke a token as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param tokenId The token to revoke. + */ + function delegateRevoke(uint256 tokenId) external; + + /** + * @notice Revoke multiple tokens as a delegate. + * @dev MUST revert if the caller is not delegated. + * @param tokenIds The tokens to revoke. + */ + function delegateRevokeBatch(uint256[] memory tokenIds) external; + + /** + * @notice Create a delegate request describing the `soul`, `value` and `slot` of a token. + * @param soul The soul of the delegate request. + * @param value The value of the delegate request. + * @param slot The slot of the delegate request. + * @return delegateRequestId The id of the delegate request + */ + function createDelegateRequest( + address soul, + uint256 value, + uint256 slot + ) external returns (uint256 delegateRequestId); + + /** + * @notice Remove a delegate request. + * @dev MUST revert if the delegate request does not exists. + * MUST revert if the caller is not the creator of the delegate request. + * @param delegateRequestId The delegate request to remove. + */ + function removeDelegateRequest(uint256 delegateRequestId) external; +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol b/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol new file mode 100644 index 00000000000000..a0a5f9e36006c4 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol @@ -0,0 +1,57 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Enumerable Interface + * @dev This extension allows querying the tokens of a soul. + */ +interface IERC5727Enumerable is IERC5727 { + /** + * @notice Get the total number of tokens emitted. + * @return The total number of tokens emitted + */ + function emittedCount() external view returns (uint256); + + /** + * @notice Get the total number of souls. + * @return The total number of souls + */ + function soulsCount() external view returns (uint256); + + /** + * @notice Get the tokenId with `index` of the `soul`. + * @dev MUST revert if the `index` exceed the number of tokens owned by the `soul`. + * @param soul The soul whose token is queried for. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenOfSoulByIndex(address soul, uint256 index) + external + view + returns (uint256); + + /** + * @notice Get the tokenId with `index` of all the tokens. + * @dev MUST revert if the `index` exceed the total number of tokens. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenByIndex(uint256 index) external view returns (uint256); + + /** + * @notice Get the number of tokens owned by the `soul`. + * @dev MUST revert if the `soul` does not have any token. + * @param soul The soul whose balance is queried for + * @return The number of tokens of the `soul` + */ + function balanceOf(address soul) external view returns (uint256); + + /** + * @notice Get if the `soul` owns any valid tokens. + * @param soul The soul whose valid token infomation is queried for + * @return if the `soul` owns any valid tokens + */ + function hasValid(address soul) external view returns (bool); +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Expirable.sol b/assets/eip-5727/contracts/interfaces/IERC5727Expirable.sol new file mode 100644 index 00000000000000..25336a5117e9e5 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Expirable.sol @@ -0,0 +1,48 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Expirable Interface + * @dev This extension allows soulbound tokens to be expired. + */ +interface IERC5727Expirable is IERC5727 { + /** + * @notice Get the expire date of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token for which the expiry date is queried + * @return The expiry date of the token + */ + function expiryDate(uint256 tokenId) external view returns (uint256); + + /** + * @notice Get if a token is expired. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token for which the expired status is queried + * @return If the token is expired + */ + function isExpired(uint256 tokenId) external view returns (bool); + + /** + * @notice Set the expiry date of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * MUST revert if the `date` is in the past. + * @param tokenId The token whose expiry date is set + * @param date The expire date to set + */ + function setExpiryDate(uint256 tokenId, uint256 date) external; + + /** + * @notice Set the expiry date of multiple tokens. + * @dev MUST revert if the `tokenIds` tokens does not exist. + * MUST revert if the `dates` is in the past. + * MUST revert if the length of `tokenIds` and `dates` do not match. + * @param tokenIds The tokens whose expiry dates are set + * @param dates The expire dates to set + */ + function setBatchExpiryDates( + uint256[] memory tokenIds, + uint256[] memory dates + ) external; +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol b/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol new file mode 100644 index 00000000000000..72a173edd7ebc2 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Consensus Interface + * @dev This extension allows minting and revocation of tokens by community voting. + */ +interface IERC5727Governance is IERC5727 { + /** + * @notice Get the voters of the contract. + * @return The array of the voters + */ + function voters() external view returns (address[] memory); + + /** + * @notice Approve to mint the token described by the `approvalRequestId` to `soul`. + * @dev MUST revert if the caller is not a voter. + * @param soul The soul which the token to mint to + * @param approvalRequestId The approval request describing the value and slot of the token to mint + */ + function approveMint(address soul, uint256 approvalRequestId) external; + + /** + * @notice Approve to revoke the `tokenId`. + * @dev MUST revert if the `tokenId` does not exist. + * @param tokenId The token to revert + */ + function approveRevoke(uint256 tokenId) external; + + /** + * @notice Create an approval request describing the `value` and `slot` of a token. + * @dev MUST revert when `value` is zero. + * @param value The value of the approval request to create + */ + function createApprovalRequest(uint256 value, uint256 slot) external; + + /** + * @notice Remove `approvalRequestId` approval request. + * @dev MUST revert if the caller is not the creator of the approval request. + * @param approvalRequestId The approval request to remove + */ + function removeApprovalRequest(uint256 approvalRequestId) external; + + /** + * @notice Add a new voter `newVoter`. + * @dev MUST revert if the caller is not an administrator. + * MUST revert if `newVoter` is already a voter. + * @param newVoter the new voter to add + */ + function addVoter(address newVoter) external; + + /** + * @notice Remove the `voter` from the contract. + * @dev MUST revert if the caller is not an administrator. + * MUST revert if `voter` is not a voter. + * @param voter the voter to remove + */ + function removeVoter(address voter) external; +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Metadata.sol b/assets/eip-5727/contracts/interfaces/IERC5727Metadata.sol new file mode 100644 index 00000000000000..895ce3c5cc41c4 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Metadata.sol @@ -0,0 +1,44 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Metadata Interface + * @dev This extension allows querying the metadata of soulbound tokens. + */ +interface IERC5727Metadata is IERC5727 { + /** + * @notice Get the name of the contract. + * @return The name of the contract + */ + function name() external view returns (string memory); + + /** + * @notice Get the symbol of the contract. + * @return The symbol of the contract + */ + function symbol() external view returns (string memory); + + /** + * @notice Get the URI of a token. + * @dev MUST revert if the `tokenId` token does not exist. + * @param tokenId The token whose URI is queried for + * @return The URI of the `tokenId` token + */ + function tokenURI(uint256 tokenId) external view returns (string memory); + + /** + * @notice Get the URI of the contract. + * @return The URI of the contract + */ + function contractURI() external view returns (string memory); + + /** + * @notice Get the URI of a slot. + * @dev MUST revert if the `slot` does not exist. + * @param slot The slot whose URI is queried for + * @return The URI of the `slot` + */ + function slotURI(uint256 slot) external view returns (string memory); +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol b/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol new file mode 100644 index 00000000000000..ce715d07251732 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Recovery Interface + * @dev This extension allows recovering soulbound tokens from an address provided its signature. + */ +interface IERC5727Recovery is IERC5727 { + /** + * @notice Recover the tokens of `soul` with `signature`. + * @dev MUST revert if the signature is invalid. + * @param soul The soul whose tokens are recovered + * @param signature The signature signed by the `soul` + */ + function recover(address soul, bytes memory signature) external; +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Shadow.sol b/assets/eip-5727/contracts/interfaces/IERC5727Shadow.sol new file mode 100644 index 00000000000000..fd4bab93b08eaa --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727Shadow.sol @@ -0,0 +1,31 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; + +/** + * @title ERC5727 Soulbound Token Shadow Interface + * @dev This extension allows restricting the visibility of specific soulbound tokens. + */ +interface IERC5727Shadow is IERC5727 { + /** + * @notice Shadow a token. + * @dev MUST revert if the `tokenId` token does not exists. + * @param tokenId The token to shadow + */ + function shadow(uint256 tokenId) external; + + /** + * @notice Reveal a token. + * @dev MUST revert if the `tokenId` token does not exists. + * @param tokenId The token to reveal + */ + function reveal(uint256 tokenId) external; + + /** + * @notice Get if a token is shadowed. + * @dev MUST revert if the `tokenId` token does not exists. + * @param tokenId The token to query + */ + function isShadowed(uint256 tokenId) external returns (bool); +} diff --git a/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol b/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol new file mode 100644 index 00000000000000..fa224bcb8d1662 --- /dev/null +++ b/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol @@ -0,0 +1,45 @@ +//SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC5727.sol"; +import "./IERC5727Enumerable.sol"; + +/** + * @title ERC5727 Soulbound Token Slot Enumerable Interface + * @dev This extension allows querying information about slots. + */ +interface IERC5727SlotEnumerable is IERC5727, IERC5727Enumerable { + /** + * @notice Get the total number of slots. + * @return The total number of slots. + */ + function slotCount() external view returns (uint256); + + /** + * @notice Get the slot with `index` among all the slots. + * @dev MUST revert if the `index` exceed the total number of slots. + * @param index The index of the slot queried for + * @return The slot is queried for + */ + function slotByIndex(uint256 index) external view returns (uint256); + + /** + * @notice Get the number of tokens in a slot. + * @dev MUST revert if the slot does not exist. + * @param slot The slot whose number of tokens is queried for + * @return The number of tokens in the `slot` + */ + function tokenSupplyInSlot(uint256 slot) external view returns (uint256); + + /** + * @notice Get the tokenId with `index` of the `slot`. + * @dev MUST revert if the `index` exceed the number of tokens in the `slot`. + * @param slot The slot whose token is queried for. + * @param index The index of the token queried for + * @return The token is queried for + */ + function tokenInSlotByIndex(uint256 slot, uint256 index) + external + view + returns (uint256); +} diff --git a/assets/eip-5727/hardhat.config.ts b/assets/eip-5727/hardhat.config.ts new file mode 100644 index 00000000000000..9f381a947574b1 --- /dev/null +++ b/assets/eip-5727/hardhat.config.ts @@ -0,0 +1,16 @@ +import '@nomicfoundation/hardhat-toolbox' +import { HardhatUserConfig } from 'hardhat/types' + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 1000, + }, + }, + }, +} + +export default config diff --git a/assets/eip-5727/package.json b/assets/eip-5727/package.json new file mode 100644 index 00000000000000..1a8e1a57bc1a06 --- /dev/null +++ b/assets/eip-5727/package.json @@ -0,0 +1,17 @@ +{ + "name": "eip-5727", + "version": "0.0.1", + "description": "EIP-5727: Semi-Fungible Soulbound Token", + "author": "Austin Zhu", + "dependencies": { + "@openzeppelin/contracts": "4.7.3" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "2.0.0", + "@typechain/hardhat": "6.1.3", + "@types/node": "18.7.13", + "hardhat": "2.11.2", + "ts-node": "10.9.1", + "typescript": "4.8.4" + } +} diff --git a/assets/eip-5727/test/ERC5727Example.test.ts b/assets/eip-5727/test/ERC5727Example.test.ts new file mode 100644 index 00000000000000..d29cac571a4c74 --- /dev/null +++ b/assets/eip-5727/test/ERC5727Example.test.ts @@ -0,0 +1,687 @@ +import { expect } from 'chai' +import { ethers } from 'hardhat' +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' +import { ERC5727Example, ERC5727Example__factory } from '../typechain' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' + +interface Fixture { + ERC5727ExampleFactory: ERC5727Example__factory + ERC5727ExampleContract: ERC5727Example + owner: SignerWithAddress + tokenOwnerSoul1: SignerWithAddress + tokenOwnerSoul2: SignerWithAddress + voterSoul1: SignerWithAddress + voterSoul2: SignerWithAddress + delegateSoul1: SignerWithAddress + delegateSoul2: SignerWithAddress +} + +describe('ERC5727Test', function () { + async function deployTokenFixture(): Promise { + const ERC5727ExampleFactory = await ethers.getContractFactory('ERC5727Example') + const [ + owner, + tokenOwnerSoul1, + tokenOwnerSoul2, + voterSoul1, + voterSoul2, + delegateSoul1, + delegateSoul2, + ] = await ethers.getSigners() + const ERC5727ExampleContract = await ERC5727ExampleFactory.deploy( + 'Soularis', + 'SOUL', + [voterSoul1.address, voterSoul2.address], + 'https://soularis-demo.s3.ap-northeast-1.amazonaws.com/perk/', + ) + await ERC5727ExampleContract.deployed() + return { + ERC5727ExampleFactory, + ERC5727ExampleContract, + owner, + tokenOwnerSoul1, + tokenOwnerSoul2, + voterSoul1, + voterSoul2, + delegateSoul1, + delegateSoul2, + } + } + + describe('ERC5727Example', function () { + it('Only contract owner can mint with mint', async function () { + const { ERC5727ExampleContract, owner, tokenOwnerSoul1, voterSoul1 } = await loadFixture( + deployTokenFixture, + ) + expect(await ERC5727ExampleContract.owner()).equal(owner.address) + + await ERC5727ExampleContract.connect(owner).mint( + tokenOwnerSoul1.address, + 1, + 1, + 2664539263, + false, + ) + + await expect( + ERC5727ExampleContract.connect(voterSoul1).mint( + tokenOwnerSoul1.address, + 1, + 2, + 2664539263, + false, + ), + ).be.reverted + }) + + it('Only contract owner can revoke with revoke', async function () { + const { ERC5727ExampleContract, owner, tokenOwnerSoul1, voterSoul1 } = await loadFixture( + deployTokenFixture, + ) + expect(await ERC5727ExampleContract.owner()).equal(owner.address) + + await ERC5727ExampleContract.connect(owner).mint( + tokenOwnerSoul1.address, + 1, + 1, + 2664539263, + false, + ) + + await ERC5727ExampleContract.connect(owner).revoke(0) + + await expect(ERC5727ExampleContract.connect(voterSoul1).revoke(0)).be.reverted + }) + + it('Balance of souls will increase when tokens are minted to them', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + + expect(await ERC5727ExampleContract.balanceOf(tokenOwnerSoul1.address)).equal(3) + + expect(await ERC5727ExampleContract.balanceOf(tokenOwnerSoul2.address)).equal(1) + }) + + it('Token will be invalid if it is revoked', async function () { + const { ERC5727ExampleContract, owner, tokenOwnerSoul1 } = await loadFixture( + deployTokenFixture, + ) + expect(await ERC5727ExampleContract.owner()).equal(owner.address) + + await ERC5727ExampleContract.connect(owner).mint( + tokenOwnerSoul1.address, + 1, + 1, + 2664539263, + false, + ) + + await ERC5727ExampleContract.connect(owner).revoke(0) + + expect(await ERC5727ExampleContract.isValid(0)).equal(false) + }) + + it('Revert if a token not exist is revoked', async function () { + const { ERC5727ExampleContract } = await loadFixture(deployTokenFixture) + await expect(ERC5727ExampleContract.revoke(100)).be.reverted + }) + + it('Support Interface', async function () { + const { ERC5727ExampleContract } = await loadFixture(deployTokenFixture) + expect(await ERC5727ExampleContract.supportsInterface('0x35f61d8a')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x3da384b4')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x211ec300')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x2a8cf5aa')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x3ba738d1')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0xba3e1a9d')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x379f4e66')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x3475cd68')).to.equal(true) + expect(await ERC5727ExampleContract.supportsInterface('0x3b741b9e')).to.equal(true) + }) + }) + + describe('ERC5727', function () { + it('The information of a token can be correctly queried', async function () { + const { ERC5727ExampleContract, owner, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + + expect(await ERC5727ExampleContract.slotOf(0)).equal(1) + expect(await ERC5727ExampleContract.soulOf(0)).equal(tokenOwnerSoul1.address) + expect(await ERC5727ExampleContract.issuerOf(0)).equal(owner.address) + expect(await ERC5727ExampleContract.isValid(0)).equal(true) + }) + + it('The information of the contract is correct', async function () { + const { ERC5727ExampleContract } = await loadFixture(deployTokenFixture) + expect(await ERC5727ExampleContract.name()).equal('Soularis') + expect(await ERC5727ExampleContract.symbol()).equal('SOUL') + }) + }) + + describe('ERC5727Delegate', function () { + it('Only contract owner can create or delete delegate request', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, voterSoul1 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await expect( + ERC5727ExampleContract.connect(voterSoul1).createDelegateRequest( + tokenOwnerSoul1.address, + 1, + 1, + ), + ).be.reverted + await expect(ERC5727ExampleContract.connect(voterSoul1).removeDelegateRequest(0)).be.reverted + await ERC5727ExampleContract.removeDelegateRequest(0) + }) + + it('Only contract owner or delegate can delegate', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, delegateSoul1, delegateSoul2 } = + await loadFixture(deployTokenFixture) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.mintDelegate(delegateSoul1.address, 0) + await ERC5727ExampleContract.connect(delegateSoul1).mintDelegate(delegateSoul2.address, 0) + await expect( + ERC5727ExampleContract.connect(delegateSoul1).mintDelegate(delegateSoul2.address, 0), + ).be.reverted + + await ERC5727ExampleContract.revokeDelegate(delegateSoul1.address, 0) + await ERC5727ExampleContract.connect(delegateSoul1).revokeDelegate(delegateSoul2.address, 0) + await expect( + ERC5727ExampleContract.connect(delegateSoul1).revokeDelegate(delegateSoul2.address, 0), + ).be.reverted + }) + + it('Only contract owner or delegate can mint or revoke', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, delegateSoul1 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.mintDelegate(delegateSoul1.address, 0) + await ERC5727ExampleContract.delegateMint(0) + await ERC5727ExampleContract.delegateMint(0) + await ERC5727ExampleContract.connect(delegateSoul1).delegateMint(0) + await expect(ERC5727ExampleContract.connect(delegateSoul1).delegateMint(0)).be.reverted + + await ERC5727ExampleContract.revokeDelegate(delegateSoul1.address, 1) + await ERC5727ExampleContract.delegateRevoke(0) + await ERC5727ExampleContract.connect(delegateSoul1).delegateRevoke(1) + await expect(ERC5727ExampleContract.connect(delegateSoul1).delegateRevoke(2)).be.reverted + }) + + it('Batch operations', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2, delegateSoul1 } = + await loadFixture(deployTokenFixture) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul2.address, 1, 1) + await ERC5727ExampleContract.mintDelegateBatch( + [delegateSoul1.address, delegateSoul1.address], + [0, 1], + ) + await ERC5727ExampleContract.connect(delegateSoul1).delegateMintBatch([0, 1]) + await ERC5727ExampleContract.delegateRevokeBatch([0, 1]) + }) + + it('Query of information of delegate request', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1 } = await loadFixture(deployTokenFixture) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + expect(await ERC5727ExampleContract.soulOfDelegateRequest(0)).to.equal( + tokenOwnerSoul1.address, + ) + expect(await ERC5727ExampleContract.valueOfDelegateRequest(0)).to.equal(1) + expect(await ERC5727ExampleContract.slotOfDelegateRequest(0)).to.equal(1) + }) + + it('Query for delegated requests or token of an operator', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2, delegateSoul1 } = + await loadFixture(deployTokenFixture) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul2.address, 1, 1) + await ERC5727ExampleContract.mintDelegateBatch( + [delegateSoul1.address, delegateSoul1.address], + [0, 1], + ) + expect(await ERC5727ExampleContract.delegatedRequestsOf(delegateSoul1.address)).to.eql([ + ethers.BigNumber.from(0), + ethers.BigNumber.from(1), + ]) + await ERC5727ExampleContract.delegateMintBatch([0, 1]) + await ERC5727ExampleContract.revokeDelegateBatch( + [delegateSoul1.address, delegateSoul1.address], + [0, 1], + ) + expect(await ERC5727ExampleContract.delegatedTokensOf(delegateSoul1.address)).to.eql([ + ethers.BigNumber.from(0), + ethers.BigNumber.from(1), + ]) + }) + }) + + describe('ERC5727Enumerable', function () { + it('EmittedCount, soulsCount, balance of a soul can be correctly queried', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + expect(await ERC5727ExampleContract.emittedCount()).equal(4) + expect(await ERC5727ExampleContract.soulsCount()).equal(2) + expect(await ERC5727ExampleContract.balanceOf(tokenOwnerSoul1.address)).equal(3) + expect(await ERC5727ExampleContract.balanceOf(tokenOwnerSoul2.address)).equal(1) + }) + + it('Can correctly query if a soul holds valid tokens', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + await ERC5727ExampleContract.revoke(0) + await ERC5727ExampleContract.revoke(3) + expect(await ERC5727ExampleContract.hasValid(tokenOwnerSoul1.address)).equal(true) + expect(await ERC5727ExampleContract.hasValid(tokenOwnerSoul2.address)).equal(false) + }) + + it('Can correctly query a token of a soul by index', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + expect(await ERC5727ExampleContract.tokenOfSoulByIndex(tokenOwnerSoul1.address, 0)).equal(0) + expect(await ERC5727ExampleContract.tokenOfSoulByIndex(tokenOwnerSoul2.address, 0)).equal(3) + }) + + it('Revert when index overflows', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + await expect(ERC5727ExampleContract.tokenOfSoulByIndex(tokenOwnerSoul1.address, 3)).be + .reverted + await expect(ERC5727ExampleContract.tokenOfSoulByIndex(tokenOwnerSoul2.address, 1)).be + .reverted + }) + }) + + describe('ERC5727Expirable', function () { + it('Query expiry date of a token and revert if the date is not set', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + expect(await ERC5727ExampleContract.expiryDate(0)).equal(2664539263) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.delegateMint(0) + await expect(ERC5727ExampleContract.expiryDate(4)).be.reverted + }) + + it('Query if a token is expired and revert if the date is not set', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + expect(await ERC5727ExampleContract.isExpired(0)).equal(false) + await ERC5727ExampleContract.createDelegateRequest(tokenOwnerSoul1.address, 1, 1) + await ERC5727ExampleContract.delegateMint(0) + await expect(ERC5727ExampleContract.isExpired(4)).be.reverted + }) + + it('Revert when setting wrong expiry date', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + await expect(ERC5727ExampleContract.setExpiryDate(0, 100000)).be.reverted + await expect(ERC5727ExampleContract.setExpiryDate(0, 2664539262)).be.reverted + }) + }) + + describe('ERC5727Governance', function () { + it('Approve to mint a token and approve to revoke the token', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, voterSoul1, voterSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.createApprovalRequest(1, 1) + await ERC5727ExampleContract.connect(voterSoul1).approveMint(tokenOwnerSoul1.address, 0) + await expect(ERC5727ExampleContract.soulOf(0)).be.reverted + await ERC5727ExampleContract.connect(voterSoul2).approveMint(tokenOwnerSoul1.address, 0) + expect(await ERC5727ExampleContract.soulOf(0)).equal(tokenOwnerSoul1.address) + expect(await ERC5727ExampleContract.isValid(0)).equal(true) + await ERC5727ExampleContract.connect(voterSoul1).approveRevoke(0) + await ERC5727ExampleContract.connect(voterSoul2).approveRevoke(0) + expect(await ERC5727ExampleContract.isValid(0)).equal(false) + }) + + it('Revert when approving an approved request', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, voterSoul1 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.createApprovalRequest(1, 1) + await ERC5727ExampleContract.connect(voterSoul1).approveMint(tokenOwnerSoul1.address, 0) + await expect( + ERC5727ExampleContract.connect(voterSoul1).approveMint(tokenOwnerSoul1.address, 0), + ).be.reverted + await ERC5727ExampleContract.connect(voterSoul1).approveRevoke(0) + await expect(ERC5727ExampleContract.connect(voterSoul1).approveRevoke(0)).be.reverted + }) + + it('Revert when a soul other than the creator try to remove an approval request', async function () { + const { ERC5727ExampleContract, voterSoul1 } = await loadFixture(deployTokenFixture) + await ERC5727ExampleContract.connect(voterSoul1).createApprovalRequest(1, 1) + await expect(ERC5727ExampleContract.removeApprovalRequest(0)).be.reverted + await ERC5727ExampleContract.connect(voterSoul1).removeApprovalRequest(0) + }) + + it('Revert when trying to remove a non voter', async function () { + const { ERC5727ExampleContract, delegateSoul1 } = await loadFixture(deployTokenFixture) + await expect(ERC5727ExampleContract.removeVoter(delegateSoul1.address)).be.reverted + }) + + it('Revert when trying to add a current voter', async function () { + const { ERC5727ExampleContract, voterSoul1 } = await loadFixture(deployTokenFixture) + await expect(ERC5727ExampleContract.addVoter(voterSoul1.address)).be.reverted + }) + + it('Only contract owner can add or remove voters', async function () { + const { ERC5727ExampleContract, voterSoul1 } = await loadFixture(deployTokenFixture) + await expect(ERC5727ExampleContract.connect(voterSoul1).removeVoter(voterSoul1.address)).be + .reverted + await ERC5727ExampleContract.removeVoter(voterSoul1.address) + await expect(ERC5727ExampleContract.connect(voterSoul1).addVoter(voterSoul1.address)).be + .reverted + await ERC5727ExampleContract.addVoter(voterSoul1.address) + }) + + it('Correctly get voters', async function () { + const { ERC5727ExampleContract, voterSoul1, voterSoul2 } = await loadFixture( + deployTokenFixture, + ) + expect(await ERC5727ExampleContract.voters()).eql([voterSoul1.address, voterSoul2.address]) + }) + }) + + describe('ERC5727Shadow', function () { + it('Only manager can shadow or reveal a token', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + await expect(ERC5727ExampleContract.connect(tokenOwnerSoul2).shadow(0)).be.reverted + await ERC5727ExampleContract.connect(tokenOwnerSoul2).shadow(3) + await ERC5727ExampleContract.shadow(0) + await expect(ERC5727ExampleContract.connect(tokenOwnerSoul1).reveal(3)).be.reverted + await ERC5727ExampleContract.connect(tokenOwnerSoul1).reveal(0) + await ERC5727ExampleContract.reveal(3) + }) + + it('Only manager can query the shadowed information of a shadowed token', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + true, + ) + await expect(ERC5727ExampleContract.connect(tokenOwnerSoul2).soulOf(0)).be.reverted + await ERC5727ExampleContract.valueOf(3) + await ERC5727ExampleContract.connect(tokenOwnerSoul2).valueOf(3) + await ERC5727ExampleContract.slotOf(3) + await ERC5727ExampleContract.connect(tokenOwnerSoul2).slotOf(3) + await ERC5727ExampleContract.isShadowed(3) + await ERC5727ExampleContract.connect(tokenOwnerSoul2).isShadowed(3) + }) + }) + + describe('ERC5727Recovery', function () { + it('Successful recovery', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + + const signature = await tokenOwnerSoul1.signMessage( + ethers.utils.arrayify( + ethers.utils.keccak256( + ethers.utils.solidityPack( + ['address', 'address'], + [tokenOwnerSoul1.address, tokenOwnerSoul2.address], + ), + ), + ), + ) + + await ERC5727ExampleContract.connect(tokenOwnerSoul2).recover( + tokenOwnerSoul1.address, + signature, + ) + + expect(await ERC5727ExampleContract.balanceOf(tokenOwnerSoul2.address)).to.equal(4) + }) + + it('Revert when the signature is invalid', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + + await ERC5727ExampleContract.mintBatch( + [ + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul1.address, + tokenOwnerSoul2.address, + ], + 1, + 1, + 2664539263, + false, + ) + + const signature = await tokenOwnerSoul1.signMessage( + ethers.utils.arrayify( + ethers.utils.keccak256( + ethers.utils.solidityPack( + ['address', 'address'], + [tokenOwnerSoul1.address, tokenOwnerSoul2.address], + ), + ), + ), + ) + + await expect(ERC5727ExampleContract.recover(tokenOwnerSoul1.address, signature)).to.be + .reverted + }) + + it('Revert when no token can be recover', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + + const signature = await tokenOwnerSoul1.signMessage( + ethers.utils.arrayify( + ethers.utils.keccak256( + ethers.utils.solidityPack( + ['address', 'address'], + [tokenOwnerSoul1.address, tokenOwnerSoul2.address], + ), + ), + ), + ) + + await expect( + ERC5727ExampleContract.connect(tokenOwnerSoul2).recover(tokenOwnerSoul1.address, signature), + ).to.be.reverted + }) + }) + + describe('ERC5727SlotEnumerable', function () { + it('Query for slot information', async function () { + const { ERC5727ExampleContract, tokenOwnerSoul1, tokenOwnerSoul2 } = await loadFixture( + deployTokenFixture, + ) + await ERC5727ExampleContract.mintBatch( + [tokenOwnerSoul1.address, tokenOwnerSoul2.address], + 1, + 1, + 2664539263, + false, + ) + await ERC5727ExampleContract.mintBatch( + [tokenOwnerSoul1.address, tokenOwnerSoul1.address, tokenOwnerSoul2.address], + 1, + 2, + 2664539263, + false, + ) + expect(await ERC5727ExampleContract.tokenSupplyInSlot(1)).to.equal(2) + expect(await ERC5727ExampleContract.slotCount()).to.equal(2) + expect(await ERC5727ExampleContract.slotByIndex(1)).to.equal(2) + expect(await ERC5727ExampleContract.tokenInSlotByIndex(2, 2)).to.equal(4) + }) + + it('Revert when index overflows', async function () { + const { ERC5727ExampleContract } = await loadFixture(deployTokenFixture) + await expect(ERC5727ExampleContract.tokenInSlotByIndex(2, 3)).to.be.reverted + await expect(ERC5727ExampleContract.slotByIndex(2)).to.be.reverted + }) + }) + + /* + describe('ERC5727Model', function () { + it('', async function (){ + const { ERC5727ExampleFactory, ERC5727ExampleContract, owner, tokenOwnerSoul1, tokenOwnerSoul2, voterSoul1, voterSoul2, delegateSoul1, delegateSoul2 } = await loadFixture(deployTokenFixture) + }) + }) + */ +}) diff --git a/assets/eip-5727/tsconfig.json b/assets/eip-5727/tsconfig.json new file mode 100644 index 00000000000000..757bca7516cb37 --- /dev/null +++ b/assets/eip-5727/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "incremental": true, + "charset": "utf8", + "noEmitOnError": true, + "resolveJsonModule": true, + "stripInternal": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "declaration": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "typechain-types"] +} diff --git a/config/.markdownlint.yaml b/config/.markdownlint.yaml new file mode 100644 index 00000000000000..2341e3888209d4 --- /dev/null +++ b/config/.markdownlint.yaml @@ -0,0 +1,200 @@ +# Rule details can be found at https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md + +default: false + +# MD001/heading-increment/header-increment - Heading levels should only increment by one level at a time +MD001: true +# MD002/first-heading-h1/first-header-h1 - First heading should be a top-level heading (deprecated) +MD002: false + +# MD003/heading-style/header-style - Heading style +MD003: + # Heading style + style: "atx" + +# MD004/ul-style - Unordered list style +MD004: + # List style + style: "consistent" + +# MD005/list-indent - Inconsistent indentation for list items at the same level +MD005: true + +# MD006/ul-start-left - Consider starting bulleted lists at the beginning of the line (deprecated) +MD006: false +# MD007/ul-indent - Unordered list indentation +MD007: false +# MD009/no-trailing-spaces - Trailing spaces +MD009: + # Spaces for line break + br_spaces: 2 + # Allow spaces for empty lines in list items + list_item_empty_lines: false + # Include unnecessary breaks + strict: false + +# MD010/no-hard-tabs - Hard tabs +MD010: false + +# MD011/no-reversed-links - Reversed link syntax +MD011: true + +# MD012/no-multiple-blanks - Multiple consecutive blank lines +MD012: false + +# MD013/line-length - Line length +# Changed from default so we can allow the paragraphs to be a single line +MD013: false + +# MD014/commands-show-output - Dollar signs used before commands without showing output +MD014: false + +# MD018/no-missing-space-atx - No space after hash on atx style heading +MD018: true + +# MD019/no-multiple-space-atx - Multiple spaces after hash on atx style heading +MD019: true + +# MD020/no-missing-space-closed-atx - No space inside hashes on closed atx style heading +MD020: true + +# MD021/no-multiple-space-closed-atx - Multiple spaces inside hashes on closed atx style heading +MD021: true + +# MD022/blanks-around-headings/blanks-around-headers - Headings should be surrounded by blank lines +MD022: true + +# MD023/heading-start-left/header-start-left - Headings must start at the beginning of the line +MD023: true + +# MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content +MD024: + # Only check sibling headings + allow_different_nesting: false + # Only check sibling headings + siblings_only: false + +# MD025/single-title/single-h1 - Multiple top-level headings in the same document +MD025: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD026/no-trailing-punctuation - Trailing punctuation in heading +MD026: false + +# MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol +MD027: false + +# MD028/no-blanks-blockquote - Blank line inside blockquote +MD028: true + +# MD029/ol-prefix - Ordered list item prefix +MD029: false + +# MD030/list-marker-space - Spaces after list markers +MD030: + # Spaces for single-line unordered list items + ul_single: 1 + # Spaces for single-line ordered list items + ol_single: 1 + # Spaces for multi-line unordered list items + ul_multi: 1 + # Spaces for multi-line ordered list items + ol_multi: 1 + +# MD031/blanks-around-fences - Fenced code blocks should be surrounded by blank lines +MD031: + # Include list items + list_items: true + +# MD032/blanks-around-lists - Lists should be surrounded by blank lines +MD032: true + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD034/no-bare-urls - Bare URL used +MD034: false + +# MD035/hr-style - Horizontal rule style +MD035: + # Horizontal rule style + style: "consistent" + +# MD036/no-emphasis-as-heading/no-emphasis-as-header - Emphasis used instead of a heading +MD036: false + +# MD037/no-space-in-emphasis - Spaces inside emphasis markers +MD037: false + +# MD038/no-space-in-code - Spaces inside code span elements +MD038: true + +# MD039/no-space-in-links - Spaces inside link text +MD039: false + +# MD040/fenced-code-language - Fenced code blocks should have a language specified +MD040: false + +# MD041/first-line-heading/first-line-h1 - First line in a file should be a top-level heading +# NOTE: Since this uses Jekyll, this setting only applies to freestanding markdown files, such as those in the assets folder +MD041: + # Heading level + level: 1 + # RegExp for matching title in front matter + front_matter_title: "^\\s*title\\s*[:=]" + +# MD042/no-empty-links - No empty links +MD042: true + +# MD043/required-headings/required-headers - Required heading structure +# Handled by EIP Walidator +MD043: false + +# MD044/proper-names - Proper names should have the correct capitalization +MD044: + # List of proper names + names: [] + # Include code blocks + code_blocks: true + # Include HTML elements + html_elements: true + +# MD045/no-alt-text - Images should have alternate text (alt text) +MD045: false + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: "consistent" + +# MD047/single-trailing-newline - Files should end with a single newline character +MD047: true + +# MD048/code-fence-style - Code fence style +MD048: + # Code fence style + style: "consistent" + +# MD049/emphasis-style - Emphasis style should be consistent +MD049: + # Emphasis style should be consistent + style: "consistent" + +# MD050/strong-style - Strong style should be consistent +MD050: + # Strong style should be consistent + style: "consistent" + +# MD051/link-fragments - Link fragments should be valid +MD051: true + +# MD052/reference-links-images - Reference links and images should use a label that is defined +MD052: true + +# MD053/link-image-reference-definitions - Link and image reference definitions should be needed +MD053: true diff --git a/eip-template.md b/eip-template.md index a561aeb07e84c0..d71731fb7b83aa 100644 --- a/eip-template.md +++ b/eip-template.md @@ -15,33 +15,42 @@ This is the suggested template for new EIPs. Note that an EIP number will be assigned by an editor. When opening a pull request to submit your EIP, please use an abbreviated title in the filename, `eip-draft_title_abbrev.md`. -The title should be 44 characters or less. It should not repeat the EIP number in title, irrespective of the category. +The title should be 44 characters or less. It should not repeat the EIP number in title, irrespective of the category. ## Abstract + Abstract is a multi-sentence (short paragraph) technical summary. This should be a very terse and human-readable version of the specification section. Someone should be able to read only the abstract to get the gist of what this specification does. ## Motivation + The motivation section should describe the "why" of this EIP. What problem does it solve? Why should someone want to implement this standard? What benefit does it provide to the Ethereum ecosystem? What use cases does this EIP address? ## Specification -The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119. -The technical specification should describe the syntax and semantics of any new feature. The specification should be detailed enough to allow competing, interoperable implementations for any of the current Ethereum platforms (go-ethereum, parity, cpp-ethereum, ethereumj, ethereumjs, and [others](https://github.com/ethereum/wiki/wiki/Clients)). +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + +The technical specification should describe the syntax and semantics of any new feature. The specification should be detailed enough to allow competing, interoperable implementations for any of the current Ethereum platforms (go-ethereum, parity, cpp-ethereum, ethereumj, ethereumjs, and [others](https://ethereum.org/en/developers/docs/nodes-and-clients/)). ## Rationale + The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g. how the feature is supported in other languages. ## Backwards Compatibility + All EIPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their severity. The EIP must explain how the author proposes to deal with these incompatibilities. EIP submissions without a sufficient backwards compatibility treatise may be rejected outright. ## Test Cases + Test cases for an implementation are mandatory for EIPs that are affecting consensus changes. If the test suite is too large to reasonably be included inline, then consider adding it as one or more files in `../assets/eip-####/`. ## Reference Implementation + An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. If the implementation is too large to reasonably be included inline, then consider adding it as one or more files in `../assets/eip-####/`. ## Security Considerations + All EIPs must contain a section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks and can be used throughout the life cycle of the proposal. E.g. include security-relevant design decisions, concerns, important discussions, implementation-specific guidance and pitfalls, an outline of threats and risks and how they are being addressed. EIP submissions missing the "Security Considerations" section will be rejected. An EIP cannot proceed to status "Final" without a Security Considerations discussion deemed sufficient by the reviewers. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/index.html b/index.html index d3f4095f64b413..8ef51c6686e5ee 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,9 @@ ---

EIPs - Discord + Discord channel for ECH eip-editer + Discord channel for Eth R&D eip-editing + Discord server for discussions about proposals that impact Ethereum wallets RSS

Ethereum Improvement Proposals (EIPs) describe standards for the Ethereum platform, including core protocol specifications, client APIs, and contract standards. Network upgrades are discussed separately in the Ethereum Project Management repository.

@@ -32,16 +34,16 @@

Standard Track ({{site.pages|where:"type","Standards Track"|size}})

Describes any change that affects most or all Ethereum implementations, such as a change to the network protocol, a change in block or transaction validity rules, proposed application standards/conventions, or any change or addition that affects the interoperability of applications using Ethereum. Furthermore Standard EIPs can be broken down into the following categories.

Core ({{site.pages|where:"type","Standards Track"|where:"category","Core"|size}})

-

Improvements requiring a consensus fork (e.g. EIP-5, EIP-101), as well as changes that are not necessarily consensus critical but may be relevant to “core dev” discussions (for example, the miner/node strategy changes 2, 3, and 4 of EIP-86).

+

Improvements requiring a consensus fork (e.g. EIP-5, EIP-211), as well as changes that are not necessarily consensus critical but may be relevant to “core dev” discussions (for example, the PoA algorithm for testnets described in EIP-225).

Networking ({{site.pages|where:"type","Standards Track"|where:"category","Networking"|size}})

-

Includes improvements around devp2p (EIP-8) and Light Ethereum Subprotocol, as well as proposed improvements to network protocol specifications of whisper and swarm.

+

Includes improvements around devp2p (EIP-8) and Light Ethereum Subprotocol, as well as proposed improvements to network protocol specifications of whisper and swarm.

Interface ({{site.pages|where:"type","Standards Track"|where:"category","Interface"|size}})

-

Includes improvements around client API/RPC specifications and standards, and also certain language-level standards like method names (EIP-6) and contract ABIs. The label “interface” aligns with the interfaces repo and discussion should primarily occur in that repository before an EIP is submitted to the EIPs repository.

+

Includes improvements around client API/RPC specifications and standards, and also certain language-level standards like method names (EIP-6) and contract ABIs. The label “interface” aligns with the interfaces repo and discussion should primarily occur in that repository before an EIP is submitted to the EIPs repository.

ERC ({{site.pages|where:"type","Standards Track"|where:"category","ERC"|size}})

-

Application-level standards and conventions, including contract standards such as token standards (ERC-20), name registries (ERC-137), URI schemes (ERC-681), library/package formats (EIP190), and wallet formats (EIP-85).

+

Application-level standards and conventions, including contract standards such as token standards (EIP-20), name registries (EIP-137), URI schemes (EIP-681), library/package formats (EIP-190), and account abstraction (EIP-4337).

Meta ({{site.pages|where:"type","Meta"|size}})

Describes a process surrounding Ethereum or proposes a change to (or an event in) a process. Process EIPs are like Standards Track EIPs but apply to areas other than the Ethereum protocol itself. They may propose an implementation, but not to Ethereum's codebase; they often require community consensus; unlike Informational EIPs, they are more than recommendations, and users are typically not free to ignore them. Examples include procedures, guidelines, changes to the decision-making process, and changes to the tools or environment used in Ethereum development. Any meta-EIP is also considered a Process EIP.

diff --git a/last-call.xml b/last-call.xml index 83e60e71434ad3..fe3d6df13fbf4e 100644 --- a/last-call.xml +++ b/last-call.xml @@ -20,7 +20,7 @@ layout: null

Please visit the [ethereum/EIPs issues to comment](https://github.com/ethereum/EIPs/issues/{{eip.eip}}).

{% endif %}
- {{ eip.content }} + {{ eip.content }} {% endcapture %} {{ eip.title | xml_escape }}