From 45ec3cd319d20e772104aedbb42f3f103dea05d5 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Fri, 2 Dec 2022 23:44:42 +0100 Subject: [PATCH 001/274] Mark EIP-4750 and EIP-5450 as Review (#6079) * Mark EIP-4750 and EIP-5450 as Review * Make linter happy --- EIPS/eip-4750.md | 2 +- EIPS/eip-5450.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 98a830d6c8c2fc..bb14ae1f2da843 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -4,7 +4,7 @@ title: EOF - Functions description: Individual sections for functions with `CALLF` and `RETF` instructions author: Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast) discussions-to: https://ethereum-magicians.org/t/eip-4750-eof-functions/8195 -status: Draft +status: Review type: Standards Track category: Core created: 2022-01-10 diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 7061c18e4f7809..4c9d6e62b6f873 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -4,7 +4,7 @@ title: EOF - Stack Validation description: Deploy-time validation of stack usage for EOF functions. author: Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast), Alex Beregszaszi (@axic) discussions-to: https://ethereum-magicians.org/t/eip-5450-eof-stack-validation/10410 -status: Draft +status: Review type: Standards Track category: Core created: 2022-08-12 @@ -59,7 +59,7 @@ Moreover, the `max_stack_height` computed during validation must be stored along ### Unreachable code -The current validation algorithm ignores _unreachable_ instructions. The algorithm can be extended to reject any code having any unreachable instructions but additional instructions traversal is needed (or more efficient algorithm must be developed). +The current validation algorithm ignores *unreachable* instructions. The algorithm can be extended to reject any code having any unreachable instructions but additional instructions traversal is needed (or more efficient algorithm must be developed). ### Clean stack upon termination @@ -67,7 +67,7 @@ It is currently required that the EVM stack is empty (in the current function co This can be used for implementing more efficient early exits from a function (e.g. assertion failure). -For "exit" instructions which terminates the whole program execution (`STOP`, `RETURN`, etc) this is no change comparing to pre-EOF EVM. I.e. some _garbage_ can be left on the stack. Cleaning the stack does not improve EVM implementation performance but makes the EVM programs potentially cost more (compiler is required to insert additional `POP` instructions). +For "exit" instructions which terminates the whole program execution (`STOP`, `RETURN`, etc) this is no change comparing to pre-EOF EVM. I.e. some *garbage* can be left on the stack. Cleaning the stack does not improve EVM implementation performance but makes the EVM programs potentially cost more (compiler is required to insert additional `POP` instructions). For `RETF` semantic would be more complicated. For `n` function outputs and `s` the stack height at `RETF` the EVM must erase `s-n` non-top stack items and move the `n` stack items to the place of erased ones. Cost of such operation may be relatively cheap but is not constant. From 9bb1f115a7cdf5db02f1786f86efcc7cc9ef3e36 Mon Sep 17 00:00:00 2001 From: eth-bot <85952233+eth-bot@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:16:09 -0800 Subject: [PATCH 002/274] (bot 1272989785) moving EIPS/eip-4762.md to stagnant (#5963) PR 5963 with changes to EIPS/eip-4762.md was created on (2022-Nov-15th@15.22.43) which is before the cutoff date of (2022-Nov-20th@00.16.7) i.e. 2 weeks ago --- EIPS/eip-4762.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4762.md b/EIPS/eip-4762.md index 9d55c333e71f50..03bcdc5c4f2633 100644 --- a/EIPS/eip-4762.md +++ b/EIPS/eip-4762.md @@ -4,7 +4,7 @@ title: Statelessness gas cost changes description: Changes the gas schedule to reflect the costs of creating a witness by requiring clients update their database layout to match. author: Guillaume Ballet (@gballet), Vitalik Buterin (@vbuterin), Dankrad Feist (@dankrad) discussions-to: https://ethereum-magicians.org/t/eip-4762-statelessness-gas-cost-changes/8714 -status: Draft +status: Stagnant type: Standards Track category: Core created: 2022-02-03 From d58de7cf47c6f7ad9fdef5e863b662c2db94a184 Mon Sep 17 00:00:00 2001 From: eth-bot <85952233+eth-bot@users.noreply.github.com> Date: Sat, 3 Dec 2022 16:16:27 -0800 Subject: [PATCH 003/274] (bot 1272989785) moving EIPS/eip-1459.md to stagnant (#5935) PR 5935 with changes to EIPS/eip-1459.md was created on (2022-Nov-15th@15.17.42) which is before the cutoff date of (2022-Nov-20th@00.16.7) i.e. 2 weeks ago --- EIPS/eip-1459.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-1459.md b/EIPS/eip-1459.md index 3e953149aa64d9..00416b0dc86487 100644 --- a/EIPS/eip-1459.md +++ b/EIPS/eip-1459.md @@ -5,7 +5,7 @@ description: Scheme for authenticated updateable Ethereum node lists via DNS. author: Felix Lange (@fjl), Péter Szilágyi (@karalabe) type: Standards Track category: Networking -status: Review +status: Stagnant created: 2018-09-26 requires: 778 discussions-to: https://github.com/ethereum/devp2p/issues/50 From 2a2289082ffe6fdcfa317895b879c4c594fca6b5 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Mon, 5 Dec 2022 00:15:47 +0100 Subject: [PATCH 004/274] Upgrade EIP-5773 to Review status (#6072) As the peer reviewer has been assigned to the EIP-5773 and is actively reviewing the proposal, the proposal is being upgraded to `Review` status. --- EIPS/eip-5773.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index d9a61a0808a988..bb146edc1d6f51 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -4,7 +4,7 @@ title: Context-Dependent Multi-Asset Tokens description: An interface for Multi-Asset tokens with context dependent asset type output controlled by owner's preference. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) discussions-to: https://ethereum-magicians.org/t/multiresource-tokens/11326 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-10-10 From eb07a03c80b3f79a78e2dbb98d866f7d216886d7 Mon Sep 17 00:00:00 2001 From: William Entriken Date: Sun, 4 Dec 2022 18:29:25 -0500 Subject: [PATCH 005/274] Move status (#6074) --- EIPS/eip-6049.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6049.md b/EIPS/eip-6049.md index 957ea6e1cc2672..d59527d5798e2f 100644 --- a/EIPS/eip-6049.md +++ b/EIPS/eip-6049.md @@ -4,7 +4,7 @@ title: Deprecate SELFDESTRUCT description: Deprecate SELFDESTRUCT by discouraging its use and warning about a potential future behavior change. author: William Entriken (@fulldecent) discussions-to: https://ethereum-magicians.org/t/deprecate-selfdestruct/11907 -status: Draft +status: Review type: Meta created: 2022-11-27 --- From 2f91ed57188d56bae58b8f6696ba56de0625606d Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Mon, 5 Dec 2022 12:53:33 +0100 Subject: [PATCH 006/274] Update EIP-4750: Clarify RETF not cleaning the stack automatically (#6080) --- EIPS/eip-4750.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index bb14ae1f2da843..604cd1a9c8b4f6 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -102,7 +102,7 @@ Under `PC_post_instruction` we mean the PC position after the entire immediate a #### `RETF` 1. Does not have immediate arguments. -2. If data stack has less than `caller_stack_height + types[code_section_index].outputs`, execution results in exceptional halt. +2. If number of items on the data stack is not equal `caller_stack_height + type[code_section_index].outputs`, execution results in exceptional halt. 3. Charges 3 gas. 4. Pops nothing and pushes nothing to data stack. 5. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. From 7eac5f7f4aeb7c6f251b5e4a5ebb92d14d307d93 Mon Sep 17 00:00:00 2001 From: George Kadianakis Date: Mon, 5 Dec 2022 14:02:25 +0200 Subject: [PATCH 007/274] EIP4844: Be explicit about precompile accepting canonical inputs (#6082) --- EIPS/eip-4844.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index b2c5a705488dbe..df8423c5b84df3 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -71,12 +71,12 @@ Compared to full data sharding, this EIP has a reduced cap on the number of thes ### Cryptographic Helpers -Throughout this proposal we use cryptographic methods and classes defined in the corresponding [consensus 4844 specs](https://github.com/ethereum/consensus-specs/blob/a45627164d34ddb60acca385e2324835fc14ca5c/specs/eip4844). +Throughout this proposal we use cryptographic methods and classes defined in the corresponding [consensus 4844 specs](https://github.com/ethereum/consensus-specs/blob/23d3aeebba3b5da0df4bd25108461b442199f406/specs/eip4844). -Specifically, we use the following methods from [`polynomial-commitments.md`](https://github.com/ethereum/consensus-specs/blob/a45627164d34ddb60acca385e2324835fc14ca5c/specs/eip4844/polynomial-commitments.md): +Specifically, we use the following methods from [`polynomial-commitments.md`](https://github.com/ethereum/consensus-specs/blob/23d3aeebba3b5da0df4bd25108461b442199f406/specs/eip4844/polynomial-commitments.md): -- [`verify_kzg_proof()`](https://github.com/ethereum/consensus-specs/blob/a45627164d34ddb60acca385e2324835fc14ca5c/specs/eip4844/polynomial-commitments.md#verify_kzg_proof) -- [`verify_aggregate_kzg_proof()`](https://github.com/ethereum/consensus-specs/blob/a45627164d34ddb60acca385e2324835fc14ca5c/specs/eip4844/polynomial-commitments.md#verify_aggregate_kzg_proof) +- [`verify_kzg_proof()`](https://github.com/ethereum/consensus-specs/blob/23d3aeebba3b5da0df4bd25108461b442199f406/specs/eip4844/polynomial-commitments.md#verify_kzg_proof) +- [`verify_aggregate_kzg_proof()`](https://github.com/ethereum/consensus-specs/blob/23d3aeebba3b5da0df4bd25108461b442199f406/specs/eip4844/polynomial-commitments.md#verify_aggregate_kzg_proof) ### Helpers @@ -243,8 +243,9 @@ The opcode has a gas cost of `HASH_OPCODE_GAS`. ### Point evaluation precompile -Add a precompile at `POINT_EVALUATION_PRECOMPILE_ADDRESS` that evaluates a proof that a particular blob resolves -to a particular value at a point. +Add a precompile at `POINT_EVALUATION_PRECOMPILE_ADDRESS` that verifies a KZG proof which claims that a blob +(represented by a commitment) evaluates to a given value at a given point. + The precompile costs `POINT_EVALUATION_PRECOMPILE_GAS` and executes the following logic: ```python @@ -270,6 +271,8 @@ def point_evaluation_precompile(input: Bytes) -> Bytes: return Bytes(U256(FIELD_ELEMENTS_PER_BLOB).to_be_bytes32() + U256(BLS_MODULUS).to_be_bytes32()) ``` +The precompile MUST reject non-canonical field elements (i.e. provided field elements MUST be strictly less than `BLS_MODULUS`). + ### Gas accounting We introduce data gas as a new type of gas. It is independent of normal gas and follows its own targeting rule, similar to EIP-1559. From fa4c0f42a4045f3882479efc974453f90ed7908a Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Mon, 5 Dec 2022 23:58:12 +0100 Subject: [PATCH 008/274] Clarifications to EIP-663 (#6086) * Clarifications to EIP-663 * Fix linter --- EIPS/eip-663.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-663.md b/EIPS/eip-663.md index 2b3f8bd0c4adee..efa0aa75b52d33 100644 --- a/EIPS/eip-663.md +++ b/EIPS/eip-663.md @@ -29,8 +29,8 @@ Introducing `SWAPN` and `DUPN` will provide an option to compilers to simplify a We introduce two new instructions: - 1. `DUPN` (`0xb3`) - 2. `SWAPN` (`0xb4`) + 1. `DUPN` (`0xb5`) + 2. `SWAPN` (`0xb6`) If the code is legacy bytecode, both of these instructions result in an *exceptional halt*. (*Note: This means no change to behaviour.*) @@ -55,7 +55,16 @@ The gas cost for both instructions is set at 3. ## Rationale -TBA +### EOF-only + +Since this instruction depends on an immediate argument encoding, it can only be enabled within EOF. In legacy bytecode that encoding could contradict jumpdest-analysis. + +### Size of immediate argument + +A 16-bit size was considered to accommodate the full stack space of 1024 items, however: + +1. that would require an additional restriction/check (`n < 1024`) +2. the 256 depth is a large improvement over the current 16 and the overhead of an extra byte would make it less useful ## Backwards Compatibility From 2b8cf837fdb498ae6b8a58b665baff199183acdc Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Tue, 6 Dec 2022 15:05:04 +0100 Subject: [PATCH 009/274] EIP-663: Mark for review (#6087) * EIP-663: Mark for review * Clarification for backwards compatibility. --- EIPS/eip-663.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-663.md b/EIPS/eip-663.md index efa0aa75b52d33..2d88e22e27903f 100644 --- a/EIPS/eip-663.md +++ b/EIPS/eip-663.md @@ -4,7 +4,7 @@ title: Unlimited SWAP and DUP instructions description: Introduce SWAPN and DUPN which take an immediate value for the depth author: Alex Beregszaszi (@axic) discussions-to: https://ethereum-magicians.org/t/eip-663-unlimited-swap-and-dup-instructions/3346 -status: Draft +status: Review type: Standards Track category: Core created: 2017-07-03 @@ -68,11 +68,17 @@ A 16-bit size was considered to accommodate the full stack space of 1024 items, ## Backwards Compatibility -This has no effect on backwards compatibility because the opcodes were not previously allocated. +This has no effect on backwards compatibility because the opcodes were not previously allocated and the feature is only enabled in EOF. ## Test Cases -TBA +For `0 <= n <= 255`: + + - `DUPN n` to fail if `stack_height < n`. + - `SWAPN n` to fail if `stack_height < (n + 1)`. + - `DUPN n` to fail if `stack_height + 1 > 1024`. + - `DUPN n` and `SWAPN n` to fail if gas available is less than 3. + - otherwise `DUPN n` should push the `stack[n]` item to the stack, and `SWAPN n` should swap `stack[n + 1]` with `stack[stack.top()]`. ## Security Considerations From 6d0e7420ba8663f486e65415b6aa451842d4dea3 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 6 Dec 2022 09:56:04 -0700 Subject: [PATCH 010/274] clarify types and allowed values for withdrawal data (#6089) --- EIPS/eip-4895.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-4895.md b/EIPS/eip-4895.md index 7a5de3ee3d0ad0..230bd7f4489871 100644 --- a/EIPS/eip-4895.md +++ b/EIPS/eip-4895.md @@ -42,9 +42,9 @@ Define a new payload-level object called a `withdrawal` that describes withdrawa `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 +2. the `validator_index` of the validator, as a `uint64` value, 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. +4. a nonzero `amount` of ether given in wei as a `uint64` value. *NOTE*: the `index` for each withdrawal is a global counter spanning the entire sequence of withdrawals. From 0fa80ec53554671d55b07f19712cbc589b4a1f33 Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Tue, 6 Dec 2022 11:29:27 -0700 Subject: [PATCH 011/274] fix typo from #6089 to use correct type for withdrawal `amount` (#6090) --- EIPS/eip-4895.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4895.md b/EIPS/eip-4895.md index 230bd7f4489871..bf45884c9940f8 100644 --- a/EIPS/eip-4895.md +++ b/EIPS/eip-4895.md @@ -44,7 +44,7 @@ Define a new payload-level object called a `withdrawal` that describes withdrawa 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, as a `uint64` value, on the consensus layer the withdrawal corresponds to 3. a recipient for the withdrawn ether `address` as a 20-byte value -4. a nonzero `amount` of ether given in wei as a `uint64` value. +4. a nonzero `amount` of ether given in wei as a `uint256` value. *NOTE*: the `index` for each withdrawal is a global counter spanning the entire sequence of withdrawals. From 9f3b3837274852aac248a53de78448287f76f999 Mon Sep 17 00:00:00 2001 From: William Entriken Date: Tue, 6 Dec 2022 16:00:23 -0500 Subject: [PATCH 012/274] Update EIP-6049: Move to Last Call (#6085) * Update eip-6049.md * Fix ordering Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6049.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-6049.md b/EIPS/eip-6049.md index d59527d5798e2f..dfcfa0a549c8c8 100644 --- a/EIPS/eip-6049.md +++ b/EIPS/eip-6049.md @@ -4,7 +4,8 @@ title: Deprecate SELFDESTRUCT description: Deprecate SELFDESTRUCT by discouraging its use and warning about a potential future behavior change. author: William Entriken (@fulldecent) discussions-to: https://ethereum-magicians.org/t/deprecate-selfdestruct/11907 -status: Review +status: Last Call +last-call-deadline: 2022-12-20 type: Meta created: 2022-11-27 --- From 29641908b600a98f3a6684275409cfe2cc9380a9 Mon Sep 17 00:00:00 2001 From: Sean Date: Wed, 7 Dec 2022 08:44:03 +1100 Subject: [PATCH 013/274] Allows for multiple sales taxes (#6092) --- EIPS/eip-5570.md | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5570.md b/EIPS/eip-5570.md index 992a8b8b0caaf7..1b33d2966c58e0 100644 --- a/EIPS/eip-5570.md +++ b/EIPS/eip-5570.md @@ -172,6 +172,11 @@ The JSON schema is composed of 2 parts. The root schema contains high level deta "description": "Digital signature by the vendor of receipts data", "type": "string" } + "extra": { + "title": "Extra", + "description": "Extra information about the business/receipt as needed", + "type": "string" + } } } ``` @@ -218,7 +223,28 @@ The JSON schema is composed of 2 parts. The root schema contains high level deta "tax": { "title": "Tax", "description": "Amount of tax charged for unit", - "type": "number" + "type": "array", + "items": { + "type": "object", + "required": ["name", "rate", "amount"], + "properties": { + "name": { + "title": "Name of Tax", + "description": "GST/PST etc", + "type": "string" + }, + "rate": { + "title": "Tax Rate", + "description": "Tax rate as a percentage", + "type": "number" + }, + "amount": { + "title": "Tax Amount", + "description": "Total amount of tax charged", + "type": "number" + } + } + } }, "quantity": { "title": "Quantity", From 4de49ca6385fed6fb286e5c5db5efce340ccfbc2 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Wed, 7 Dec 2022 10:37:17 +0000 Subject: [PATCH 014/274] EIP-5646: Require only mutable state properties to be included in the state fingerprint (#6095) * Add EIP-5646: Token state fingerprint * Update EIP-5646: add link to discussion * Update EIP-5646: reference ERC standards as EIP-N * Update EIP-5646: update all EIP references to links * Update EIP-5646: fix incorrect EIP references * Update EIP-5646: Rework the structure of the whole draft * EIP-5646: Use mutable tokens instead of derivative tokens in spec * EIP-5646: Require only mutable state properties to be inlcuded in state fingerprint --- EIPS/eip-5646.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-5646.md b/EIPS/eip-5646.md index 4eab226a3b6d76..90aee881188ccd 100644 --- a/EIPS/eip-5646.md +++ b/EIPS/eip-5646.md @@ -13,7 +13,7 @@ requires: 165 ## Abstract -This specification defines the minimum interface required to unambiguously identify the state of a derivative token without knowledge of implementation details. +This specification defines the minimum interface required to unambiguously identify the state of a mutable token without knowledge of implementation details. ## Motivation @@ -40,17 +40,19 @@ interface ERC5646 is ERC165 { - `getStateFingerprint` MUST return a different value when the token state changes. - `getStateFingerprint` MUST NOT return a different value when the token state remains the same. -- `getStateFingerprint` MUST factor in all parameters that might change the state of a token. +- `getStateFingerprint` MUST include all state properties that might change during the token lifecycle (are not immutable). - `getStateFingerprint` MAY include computed values, such as values based on a current timestamp (e.g., expiration, maturity). - `getStateFingerprint` MAY include token metadata URI. - `supportsInterface(0xf5112315)` MUST return `true`. ## Rationale -Protocols can use state fingerprints as a part of a token identifier and support derivative tokens without knowing any state implementation details. +Protocols can use state fingerprints as a part of a token identifier and support mutable tokens without knowing any state implementation details. ![](../assets/eip-5646/support-per-eip.png) +State fingerprints don't have to factor in state properties that are immutable, because they can be safely identified by a token id. + This standard is not for use cases where token state property knowledge is required, as these cases cannot escape the bottleneck problem described earlier. ## Backwards Compatibility @@ -62,8 +64,8 @@ This EIP is not introducing any backward incompatibilities. ```solidity pragma solidity ^0.8.0; -/// @title Example of a token with computed state fingerprint. -contract LPTokenComputed is ERC721, ERC5646 { +/// @title Example of a mutable token implementing state fingerprint. +contract LPToken is ERC721, ERC5646 { /// @dev Stored token states (token id => state). mapping (uint256 => State) internal states; @@ -73,8 +75,8 @@ contract LPTokenComputed is ERC721, ERC5646 { address asset2; uint256 amount1; uint256 amount2; - uint256 fee; - address operator; + uint256 fee; // Immutable + address operator; // Immutable uint256 expiration; // Parameter dependent on a block.timestamp } @@ -91,9 +93,9 @@ contract LPTokenComputed is ERC721, ERC5646 { state.asset2, state.amount1, state.amount2, - state.fee, - state.operator, - block.timestamp < state.expiration ? false : true + // state.fee don't need to be part of the fingerprint computation as it is immutable + // state.operator don't need to be part of the fingerprint computation as it is immutable + block.timestamp >= state.expiration ) ); } @@ -110,7 +112,7 @@ contract LPTokenComputed is ERC721, ERC5646 { Token state fingerprints from two different contracts may collide. Because of that, they should be compared only in the context of one token contract. -If the `getStateFingerprint` implementation does not include all parameters that could change the token state, a token owner would be able to change the token state without invalidating the token identifier. It could break the trustless assumptions of several protocols, which create, e.g., buy offers for tokens. The token owner would be potentially able to decrease the value of a derivative token before accepting an offer. +If the `getStateFingerprint` implementation does not include all parameters that could change the token state, a token owner would be able to change the token state without changing the token fingerprint. It could break the trustless assumptions of several protocols, which create, e.g., buy offers for tokens. The token owner would be able to change the state of the token before accepting an offer. ## Copyright From ab73851aaaa3f1ada52d2648383c8de57d6d7927 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Wed, 7 Dec 2022 11:58:14 +0100 Subject: [PATCH 015/274] Update EIP-4200: Add Test Cases for RJUMPV (#6096) --- EIPS/eip-4200.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index 741636d186682e..fffa56060bd3bb 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -124,28 +124,38 @@ This change poses no risk to backwards compatibility, as it is introduced at the #### Valid cases -- `RJUMP`/`RJUMPI` with `JUMPDEST` as target +- `RJUMP`/`RJUMPI`/`RJUMPV` with `JUMPDEST` as target - `relative_offset` is positive/negative/`0` -- `RJUMP`/`RJUMPI` with instruction other than `JUMPDEST` as target +- `RJUMP`/`RJUMPI`/`RJUMPV` with instruction other than `JUMPDEST` as target - `relative_offset` is positive/negative/`0` +- `RJUMPV` with various valid table sizes from 1 to 255 #### Invalid cases -- `RJUMP`/`RJUMPI` with truncated immediate -- `RJUMP`/`RJUMPI` as a final instruction in code section -- `RJUMP`/`RJUMPI` target outside of code section bounds -- `RJUMP`/`RJUMPI` target push data -- `RJUMP`/`RJUMPI` target another `RJUMP`/`RJUMPI` immediate argument +- `RJUMP`/`RJUMPI`/`RJUMPV` with truncated immediate +- `RJUMP`/`RJUMPI`/`RJUMPV` as a final instruction in code section +- `RJUMP`/`RJUMPI`/`RJUMPV` target outside of code section bounds +- `RJUMP`/`RJUMPI`/`RJUMPV` target push data +- `RJUMP`/`RJUMPI`/`RJUMPV` target another `RJUMP`/`RJUMPI`/`RJUMPV` immediate argument +- `RJUMPV` with table size 0 ### Execution -- `RJUMP`/`RJUMPI` in legacy code aborts execution +- `RJUMP`/`RJUMPI`/`RJUMPV` in legacy code aborts execution - `RJUMP` - `relative_offset` is positive/negative/`0` - `RJUMPI` - `relative_offset` is positive/negative/`0` - `condition` equals `0` - `condition` does not equal `0` +- `RJUMPV 1 relative_offset` + - `case` equals `0` + - `case` does not equal `0` +- `RJUMPV` with table containing positive, negative, `0` offsets + - `case` equals `0` + - `case` does not equal `0` + - `case` outside of table bounds (`case >= count`, fallback case) + - `case` > 255 ## Reference Implementation From c282a9bd3ed750d315abbf5483ab31db2a3e5757 Mon Sep 17 00:00:00 2001 From: Moody Salem Date: Wed, 7 Dec 2022 13:16:53 -0500 Subject: [PATCH 016/274] incorporate thread feedback to EIP-1153 (#6098) * incorporate thread feedback to EIP-1153 * fix the reference to erc20 --- EIPS/eip-1153.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/EIPS/eip-1153.md b/EIPS/eip-1153.md index 147a70b4bf23c2..3ea30688b16741 100644 --- a/EIPS/eip-1153.md +++ b/EIPS/eip-1153.md @@ -70,6 +70,10 @@ Another option to solve the problem of inter-frame communication is repricing th Another approach is to keep the refund counter for transient storage separate from the refund counter for other storage uses, and remove the refund cap for transient storage. However, that approach is more complex to implement and understand. For example, the 20% refund cap must be applied to the gas used _after_ subtracting the uncapped gas refund. Otherwise, the refund amount available subject to the 20% refund cap could be increased by executing transient storage writes. Thus it is preferable to have a separate mechanism that does not interact with the refund counter. Future hard forks can remove the complex refund behavior meant to support the transient storage use case, encouraging migration to contracts that are more efficient for the Ethereum clients to execute. +There is a known objection to the word-addressed storage-like interface of the `TSTORE` and `TLOAD` opcodes since transient storage is more akin to memory than storage in lifecycle. A byte-addressed memory-like interface is another option. The storage-like word-addressed interface is preferred due to the usefulness of mappings in combination with the transaction-scoped memory region. Often times, you will need to keep transient state with arbitrary keys, such as in the [EIP-20](./eip-20.md) temporary approval use case which uses a mapping of `(owner, spender)` to `allowance`. Mappings are difficult to implement using linear memory, and linear memory must also have dynamic gas costs. It is also more complicated to handle reverts with a linear memory. It is possible to have a memory-like interface while the underlying implementation uses a map to allow for storage in arbitrary offsets, but this would result in a third memory-storage hybrid interface that would require new code paths in compilers. + +Some think that a unique transaction identifier may obviate the need for transient storage as described in this EIP. This is a misconception: a transaction identifier used in combination with regular storage has all the same issues that motivate this EIP. The two features are orthogonal. + Relative cons of this transient storage EIP: - Does not address transient usages of storage in existing contracts @@ -242,6 +246,8 @@ However, if you only spend 1M gas allocating memory in each context, and make ca Smart contract developers should understand the lifetime of transient storage variables before use. Because transient storage is automatically cleared at the end of the transaction, smart contract developers may be tempted to avoid clearing slots as part of a call in order to save gas. However, this could prevent further interactions with the contract in the same transaction (e.g. in the case of re-entrancy locks) or cause other bugs, so smart contract developers should be careful to _only_ leave transient storage slots with nonzero values when those slots are intended to be used by future calls within the same transaction. Otherwise, these opcodes behave exactly the same as `SSTORE` and `SLOAD`, so all the usual security considerations apply especially in regard to reentrancy risk. +Smart contract developers may also be tempted to use transient storage as an alternative to in-memory mappings. They should be aware that transient storage is not discarded when a call returns or reverts, as is memory, and should prefer memory for these use cases so as not to create unexpected behavior on reentrancy in the same transaction. The necessarily high cost of transient storage over memory should already discourage this usage pattern. Most usages of in-memory mappings can be better implemented with key-sorted lists of entries, and in-memory mappings are rarely required in smart contracts (i.e. the author knows of no known use cases in production). + ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From 73357f8201d5bc0271fcf0a32e074c859e48c8e1 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Wed, 7 Dec 2022 21:26:05 +0100 Subject: [PATCH 017/274] Add EIP-6046: Replace SELFDESTRUCT with DEACTIVATE (#6046) * Add deactivate eip * Use 6046 as the number * Fix header * Fix headings * Please the linter * Clarify the 2681 change * Do not refund on selfdestruct * Clarify the transfer aspect * Depend on EIP-3529 * Apply wording suggestion for motivation * Applied wording suggestions * Depend on EIP-2929 and add note * Fix typo * Update security considerations --- EIPS/eip-6046.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 EIPS/eip-6046.md diff --git a/EIPS/eip-6046.md b/EIPS/eip-6046.md new file mode 100644 index 00000000000000..87292ad479fb48 --- /dev/null +++ b/EIPS/eip-6046.md @@ -0,0 +1,67 @@ +--- +eip: 6046 +title: Replace SELFDESTRUCT with DEACTIVATE +description: Change SELFDESTRUCT to not delete storage keys and use a special value in the account nonce to signal deactivation +author: Alex Beregszaszi (@axic) +discussions-to: https://ethereum-magicians.org/t/almost-self-destructing-selfdestruct-deactivate/11886 +status: Draft +type: Standards Track +category: Core +created: 2022-11-25 +requires: 2681, 2929, 3529 +--- + +## Abstract + +Change `SELFDESTRUCT` to not delete all storage keys, and to use a special value in the account nonce to signal *decativated* accounts. Because the semantics of revival change (storage keys may exists), we also rename the instruction to `DEACTIVATE`. + +## Motivation + +The `SELFDESTRUCT` instruction currently has a fixed price, but is unbounded in terms of how many storage/account changes it performs (it needs to delete all keys). This has been an outstanding concern for some time. + +Furthermore, with *Verkle trees*, accounts will be organised differently: account properties, including storage, will have individual keys. It will not be possible to traverse and find all used keys. This makes `SELFDESTRUCT` very challenging to support in Verkle trees. + +## Specification + +1. Change the rules introduced by [EIP-2681](./eip-2681.md) such that regular nonce increase is bound by `2^64-2` instead of `2^64-1`. This applies from genesis. + +2. The behaviour of `SELFDESTRUCT` is changed such that: + + - Does not delete any storage keys and also leave the account in place. + - Transfer the account balance to the target **and** set account balance to `0.` + - Set the account nonce to `2^64-1`. + + Note that no refund is given since [EIP-3529](./eip-3529.md). + + Note that the rules of [EIP-2929](./eip-2929.md) regarding `SELFDESTRUCT` remain unchanged. + +2. Modify account execution (triggered both via external transactions or CALL* instructions), such that execution succeeds and returns an empty buffer if the nonce equals `2^64-1`. + + - Note that the account can still receive non-executable value transfers (such as coinbase transactions or other `SELFDESTRUCT`s). + +3. Modify `CREATE2` such that it allows account creation if the nonce equals `2^64-1`. + + - Note that the account (especially code and storage) might not be empty prior to `CREATE2`. + - Note that a successful `CREATE2` will change the account code, nonce and potentially balance. + +4. Rename the `SELFDESTRUCT` instruction to `DEACTIVATE`, because the semantics of "account revival" are changed: the old storage items will remain, and newly deployed code must be aware of this. + +## Rationale + +There have been various proposals of removing `SELFDESTRUCT` and many would just outright remove the deletion capability. This breaks certain usage patterns, which the *deactivation* option leaves intact, albeit with minor changes. This only affects *newly* deployed code, and not existing one. + +All the proposals would leave data in the state, but this proposal provides the flexibility to reuse or remove storage slots one-by-one should the revived contract choose to do so. + +## Backwards Compatibility + +This EIP requires a protocol upgrade, since it modifies consensus rules. The further restriction of nonce should not have an effect on accounts, as `2^64-2` is an unfeasibly high limit. + +Contracts using the revival pattern will still work, but the code deployed during revival may need to be made aware that storage keys can already exist in the account. + +## Security Considerations + +The new behaviour of preserving storage has a potential effect on security. Contract authors must be aware and design contracts accordingly. There may be an effect on existing deployed code performing autonomous destruction and revival. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From a35509fd412f476dd02fb8bd193add31d693376d Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 8 Dec 2022 12:23:08 +0100 Subject: [PATCH 018/274] Update EIP-4200: Add short new instruction summary (#6100) --- EIPS/eip-4200.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index fffa56060bd3bb..23080249f36671 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -35,11 +35,11 @@ The main benefit of these instruction is reduced gas cost (both at deploy and ex ## Specification -We introduce two new instructions on the same block number [EIP-3540](./eip-3540.md) is activated on: +We introduce three new instructions on the same block number [EIP-3540](./eip-3540.md) is activated on: -1. `RJUMP` (0x5c) -2. `RJUMPI` (0x5d) -3. `RJUMPV` (0x5e) +1. `RJUMP` (0x5c) - relative jump +2. `RJUMPI` (0x5d) - conditional relative jump +3. `RJUMPV` (0x5e) - relative jump via jump table If the code is legacy bytecode, all of these instructions result in an *exceptional halt*. (*Note: This means no change to behaviour.*) From daa5a3ce9b4e80b216bd469820cdffa2479e57a1 Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Thu, 8 Dec 2022 13:09:29 +0100 Subject: [PATCH 019/274] Fix some typos (#6102) --- EIPS/eip-6046.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-6046.md b/EIPS/eip-6046.md index 87292ad479fb48..a4af4fed15d756 100644 --- a/EIPS/eip-6046.md +++ b/EIPS/eip-6046.md @@ -13,7 +13,7 @@ requires: 2681, 2929, 3529 ## Abstract -Change `SELFDESTRUCT` to not delete all storage keys, and to use a special value in the account nonce to signal *decativated* accounts. Because the semantics of revival change (storage keys may exists), we also rename the instruction to `DEACTIVATE`. +Change `SELFDESTRUCT` to not delete all storage keys, and to use a special value in the account nonce to signal *deactivated* accounts. Because the semantics of revival change (storage keys may exists), we also rename the instruction to `DEACTIVATE`. ## Motivation @@ -30,10 +30,8 @@ Furthermore, with *Verkle trees*, accounts will be organised differently: accoun - Does not delete any storage keys and also leave the account in place. - Transfer the account balance to the target **and** set account balance to `0.` - Set the account nonce to `2^64-1`. - - Note that no refund is given since [EIP-3529](./eip-3529.md). - - Note that the rules of [EIP-2929](./eip-2929.md) regarding `SELFDESTRUCT` remain unchanged. + - Note that no refund is given since [EIP-3529](./eip-3529.md). + - Note that the rules of [EIP-2929](./eip-2929.md) regarding `SELFDESTRUCT` remain unchanged. 2. Modify account execution (triggered both via external transactions or CALL* instructions), such that execution succeeds and returns an empty buffer if the nonce equals `2^64-1`. From 49c14e30e92de83c04d4abe624527db12b85e2b0 Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 8 Dec 2022 14:52:48 +0100 Subject: [PATCH 020/274] Update EIP-4750: Add JUMPF instruction (#6023) * Update EIP-4750: Add TAILCALLF instruction * Rename TAILCALLF to JUMPF * Implement JUMPF semantics * Implement RETF with memmove * Perform stack cleanup on JUMPF too * Fix typo * Mention RJUMPV immediate validation * Add opcode descriptions * Fix JUMPF cleaning stack rule * Add rationale for JUMPF cleaning the stack * Add rationale for RETF cleaning * Fix typo in JUMPF spec Co-authored-by: Alex Beregszaszi --- EIPS/eip-4750.md | 80 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 604cd1a9c8b4f6..3ca0e8bf541af8 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -1,7 +1,7 @@ --- eip: 4750 title: EOF - Functions -description: Individual sections for functions with `CALLF` and `RETF` instructions +description: Individual sections for functions with `CALLF`, `JUMPF` and `RETF` instructions author: Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast) discussions-to: https://ethereum-magicians.org/t/eip-4750-eof-functions/8195 status: Review @@ -13,7 +13,7 @@ requires: 3540, 3670 ## Abstract -Introduce the ability to have several code sections in EOF-formatted ([EIP-3540](./eip-3540.md)) bytecode, each one representing a separate subroutine/function. Two new opcodes,`CALLF` and `RETF`, are introduced to call and return from such a function. Dynamic jump instructions are disallowed. +Introduce the ability to have several code sections in EOF-formatted ([EIP-3540](./eip-3540.md)) bytecode, each one representing a separate subroutine/function. Two new opcodes,`CALLF` and `RETF`, are introduced to call and return from such a function. Additionally `JUMPF` instruction is introduced to perform a jump to a function. Dynamic jump instructions are disallowed. ## Motivation @@ -62,12 +62,13 @@ Additionally, EVM keeps track of the index of currently executing section - `cur ### New instructions -We introduce two new instructions: +We introduce three new instructions: -1. `CALLF` (`0xb0`) -2. `RETF` (`0xb1`) +1. `CALLF` (`0xb0`) - call a function +2. `RETF` (`0xb1`) - return from a function +2. `JUMPF` (`0xb2`) - jump to a function (without updating return stack) -If the code is legacy bytecode, both of these instructions result in an *exceptional halt*. (*Note: This means no change to behaviour.*) +If the code is legacy bytecode, any of these instructions results in an *exceptional halt*. (*Note: This means no change to behaviour.*) First we define several helper values: @@ -80,7 +81,7 @@ If the code is valid EOF1, the following execution rules apply: #### `CALLF` 1. Has one immediate argument,`code_section_index`, encoded as a 16-bit unsigned big-endian value. -2. If data stack has less than `caller_stack_height + type[code_section_index].inputs`, execution results in exceptional halt. +2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. 3. If data stack size after the call would exceed `1024` items, (i.e. if `caller_stack_height - type[code_section_index].inputs + type[code_section_index].ouputs > 1024`), execution results in exceptional halt. 4. If return stack already has `1024` items, execution results in exceptional halt. 5. Charges 5 gas. @@ -99,13 +100,24 @@ Under `PC_post_instruction` we mean the PC position after the entire immediate a 7. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. +#### `JUMPF` + +1. Has one immediate argument,`code_section_index`, encoded as a 16-bit unsigned big-endian value. +2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. +3. If data stack has more than `caller_stack_height + type[code_section_index].inputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].inputs` items, so that there are exactly `caller_stack_height + type[code_section_index].inputs` items left. +4. Charges 4 gas. +5. Pops nothing and pushes nothing to data stack. +6. Pushes nothing to return stack. +7. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. + #### `RETF` 1. Does not have immediate arguments. -2. If number of items on the data stack is not equal `caller_stack_height + type[code_section_index].outputs`, execution results in exceptional halt. -3. Charges 3 gas. -4. Pops nothing and pushes nothing to data stack. -5. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. +2. If data stack has less than `caller_stack_height + type[code_section_index].outputs` items, execution results in exceptional halt. +3. If data stack has more than `caller_stack_height + type[code_section_index].outputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].outputs` items, so that there are exactly `caller_stack_height + type[code_section_index].outputs` items left. +4. Charges 4 gas. +5. Pops nothing and pushes nothing to data stack. +6. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. 1. If return stack is empty after this, execution halts with success. ### Code Validation @@ -113,11 +125,12 @@ Under `PC_post_instruction` we mean the PC position after the entire immediate a In addition to container format validation rules above, we extend code section validation rules (as defined in [EIP-3670](./eip-3670.md)). 1. Code validation rules of EIP-3670 are applied to every code section. -2. List of allowed *terminating instructions* in EIP-3670 is extended to include `RETF`. (*Note that `CALLF`, like other instructions with immediates, cannot be truncated.*) -3. Code section is invalid in case an immediate argument of any `CALLF` is greater than or equal to the total number of code sections. -4. `RJUMP` and `RJUMPI` immediate argument value (jump destination relative offset) validation: +2. List of allowed *terminating instructions* in EIP-3670 is extended to include `RETF` and `JUMPF`. (*Note that `CALLF` and `JUMPF`, like other instructions with immediates, cannot be truncated.*) +3. Code section is invalid in case an immediate argument of any `CALLF` or `JUMPF` is greater than or equal to the total number of code sections. +4. Code section is invalid in case an immediate argument of any `JUMPF` is such that `type[callee_section_index].outputs != type[caller_section_index].outputs`, i.e. it is allowed to only jump to functions with the same output type. +5. `RJUMP`, `RJUMPI` and `RJUMPV` immediate argument value (jump destination relative offset) validation: 1. Code section is invalid in case offset points to a position outside of section bounds. - 2. Code section is invalid in case offset points to one of two bytes directly following `CALLF` instruction. + 2. Code section is invalid in case offset points to one of two bytes directly following `CALLF` or `JUMPF` instruction. ### Disallowed instructions @@ -154,6 +167,22 @@ Instead of deprecating `JUMPDEST` we repurpose it as `NOP` instruction, because The purpose of `JUMPDEST` analysis was to find in code the valid `JUMPDEST` bytes that do not happen to be inside `PUSH` immediate data. Only dynamic jump instructions (`JUMP`, `JUMPI`) required destination to be `JUMPDEST` instruction. Relative static jumps (`RJUMP` and `RJUMPI`) do not have this requirement and are validated once at deploy-time EOF instruction validation. Therefore, without dynamic jump instructions, `JUMPDEST` analysis is not required. +### `JUMPF` instruction cleaning the stack + +In case function pushes on the stack more items than inputs required by the jumped-to function, these extra items could be accessed by the jumped-to function, because the underflow check is defined in terms of `caller_stack_height`, which does not change after `JUMPF`: + +> 3. If any instruction would access a data stack item below `caller_stack_height`, execution results in exceptional halt. + +In other words the entire stack frame of the function executing `JUMPF` is accessible to the jumped-to function. + +We believe this introduces unwanted edge-case behaviour with underflow exception depending on how the function was called or jumped-to, and require `JUMPF` to discard extra items to prevent this. + +### `RETF` instruction cleaning the stack + +The stack for the return items for `RETF` is automatically cleaned up. This could be relaxed and the onus could be put onto the user to clean it up, because the stack height difference can be clearly calculated, both when a function is entered via `CALLF` or `JUMPF`. + +However, we hope to relax the clean up requirement for `JUMPF`, and that would mean it is not possible anymore for the user to cleanup for `RETF`, because the height could be different depending on the path a function is entered. + ## Backwards Compatibility This change poses no risk to backwards compatibility, as it is introduced only for EOF1 contracts, for which deploying undefined instructions is not allowed, therefore there are no existing contracts using these instructions. The new instructions are not introduced for legacy bytecode (code which is not EOF formatted). @@ -184,18 +213,19 @@ valid_opcodes = [ *range(0x80, 0x8f + 1), *range(0x90, 0x9f + 1), *range(0xa0, 0xa4 + 1), - 0xb0, 0xb1, + 0xb0, 0xb1, 0xb2, # Note: 0xfe is considered assigned. 0xf0, 0xf1, 0xf3, 0xf4, 0xf5, 0xfa, 0xfd, 0xfe ] -# STOP, RETF, RETURN, REVERT, INVALID -terminating_opcodes = [0x00, 0xb1, 0xf3, 0xfd, 0xfe] +# STOP, RETF, JUMPF, RETURN, REVERT, INVALID +terminating_opcodes = [0x00, 0xb1, 0xb2, 0xf3, 0xfd, 0xfe] immediate_sizes = 256 * [0] immediate_sizes[0x5c] = 2 # RJUMP immediate_sizes[0x5d] = 2 # RJUMPI immediate_sizes[0xb0] = 2 # CALLF +immediate_sizes[0xb2] = 2 # JUMPF for opcode in range(0x60, 0x7f + 1): # PUSH1..PUSH32 immediate_sizes[opcode] = opcode - 0x60 + 1 @@ -272,7 +302,7 @@ def validate_eof(code: bytes): raise ValidationException("invalid type of section 0") # Raises ValidationException on invalid code -def validate_code_section(code: bytes, num_code_sections: int): +def validate_code_section(func_id: int, code: bytes, types: list[FunctionType] = [FunctionType(0, 0)]): # Note that EOF1 already asserts this with the code section requirements assert len(code) > 0 @@ -302,9 +332,19 @@ def validate_code_section(code: bytes, num_code_sections: int): raise ValidationException("truncated CALLF immediate") section_id = int.from_bytes(code[pos:pos+2], byteorder = "big", signed = False) - if section_id >= num_code_sections: + if section_id >= len(types): + raise ValidationException("invalid section id") + elif opcode == 0xb2: + if pos + 2 > len(code): + raise ValidationException("truncated JUMPF immediate") + section_id = int.from_bytes(code[pos:pos+2], byteorder = "big", signed = False) + + if section_id >= len(types): raise ValidationException("invalid section id") + if types[section_id].outputs != types[func_id].outputs: + raise ValidationException("incompatible function type for JUMPF") + # Save immediate value positions immediates.update(range(pos, pos + immediate_sizes[opcode])) # Skip immediates From acd69bd27119ccf5aca6546f3070ff4186e72e4f Mon Sep 17 00:00:00 2001 From: Andrei Maiboroda Date: Thu, 8 Dec 2022 14:56:35 +0100 Subject: [PATCH 021/274] Update EIP-5450: Add JUMPF simplification (#6101) * Update EIP-5450: Add JUMPF simplification change * Apply stylistic suggestions and typo fixes from review Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-5450.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 4c9d6e62b6f873..9a590b80f74211 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -50,6 +50,20 @@ Given new deploy-time guarantees, EVM implementation is not required anymore to Stack overflow check, on the other hand, is still required at run-time, because function execution can start at arbitrary (i.e. known only at run-time) stack height at `CALLF` instruction of a caller (i.e. each execution can be in arbitrary inner call frame). Verification algorithm examines only stack height changes relative to starting stack height of the function. +#### JUMPF changes + +In case a function pushed more items to the stack than is required as inputs by the jumped-to function, the previously defined `JUMPF` instruction behaviour was to remove the extra items: + +> 3. If data stack has more than `caller_stack_height + type[code_section_index].inputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].inputs`, so that there are exactly `caller_stack_height + type[code_section_index].items` items left. + +Given the new deploy-time guarantee of no function underflowing its stack frame, `JUMPF` instruction can check only that there is enough items for input arguments of the callee on the stack, without making sure there is exactly `inputs` items and not more. + +Therefore, the previously defined behavior of discarding extra items is removed, and only the check for enough inputs is done: + +> 2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. + +With this change `JUMPF` operation complexity does not depend on `ouputs` value and is constant-time, therefore the price of `JUMPF` is lowered to 3 gas. + ## Rationale ### Stack overflow check only in CALLF From 4a85c28ffa765109ae27f0f92e9f07118b1d6dd9 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 8 Dec 2022 21:07:14 -0800 Subject: [PATCH 022/274] Update ci.yml for ubuntu version fixing HTMLProofer (#6107) * Update ci.yml for ruby version fixing HTMLProofer * Update ci.yml * Update ci.yml * Update ci.yml * Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74e7087fad5565..00b9a8247d6756 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: htmlproofer: name: HTMLProofer - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Checkout EIP Repository @@ -46,7 +46,7 @@ jobs: - name: Install OpenSSL run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev - + - name: Install Ruby uses: ruby/setup-ruby@08245253a76fa4d1e459b7809579c62bd9eb718a with: From 2802db1c80cf75fad4432bd764d375dce7069356 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 8 Dec 2022 21:22:33 -0800 Subject: [PATCH 023/274] Update EIP-5247 (#6106) * Update EIP * Update EIP * Update EIP --- EIPS/eip-5247.md | 55 ++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/EIPS/eip-5247.md b/EIPS/eip-5247.md index 59a2aa45731beb..8d2823c9737a4d 100644 --- a/EIPS/eip-5247.md +++ b/EIPS/eip-5247.md @@ -1,20 +1,22 @@ --- eip: 5247 -title: Smart Proposal -description: An interface of "proposals" that is submitted to, recorded on and possibly executed and enforced onchain. +title: Smart Contract Executable Proposal Interface +description: An interface to create and execute proposals. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc-5247-executable-proposal-standard/9938 status: Draft type: Standards Track category: ERC created: 2022-07-13 -requires: 7 --- ## Abstract -This EIP presents an interface for "smart proposals": proposals that are submitted to, recorded on, and possibly executed on-chain. When a smart proposal is on-chain executable, it contains information on smart-contract calls, including addresses, ether values, call data, and other metadata information. + +This EIP presents an interface for "smart contract executable proposals": proposals that are submitted to, recorded on, and possibly executed on-chain. Such proposals include a series of information about +function calls including the target contract address, ether value to be transmitted, gas limits and calldatas. ## Motivation + It is oftentimes necessary to separate the code that is to be executed from the actual execution of the code. A typical use case for this EIP is in a Decentralized Autonomous Organization (DAO). A proposer will create a smart proposal and advocate for it. Members will then choose whether or not to endorse the proposal and vote accordingly (see [EIP-1202](./eip-1202.md)). Finallym when consensus has been formed, the proposal is executed. @@ -24,44 +26,40 @@ A second typical use-case is that one could have someone who they trust, such as A third use-case is that a person could make an "offer" to a second person, potentially with conditions. The smart proposal can be presented as an offer and the second person can execute it if they choose to accept this proposal. ## 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 +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; -interface IERC5247SmartProposal { +interface IERC5247 { event ProposalCreated( - uint256 proposalId, - string proposalUri, - uint8[] optionIds, - address proposer, - address[][] targets, - uint256[][] values, - string[][] signatures, - bytes[][] calldatas, - uint256 startBlock, - uint256 endBlock + address indexed proposer, + uint256 indexed proposalId, + address[] targets, + uint256[] values, + uint256[] gasLimits, + bytes[] calldatas, + bytes extraParams + ); + + event ProposalExecuted( + address indexed executor, + uint256 indexed proposalId, + bytes extraParams ); - event ProposalExecuted(uint256 proposalId); - // TODO: add Proposal Cancel/Edit/Withdraw Event and Functions? - // TODO: decide whether we require generating ProposalId in the method or not? - // TODO: if require generating ProposalId internally, can it be incremental hash-generated? - // TODO: what if proposal need to demonstrate sufficient support? How to input quorum? function createProposal( uint256 proposalId, - string calldata proposalUri, - uint8[] calldata optionIds, address[] calldata targets, uint256[] calldata values, + uint256[] calldata gasLimits, bytes[] calldata calldatas, - uint256 startblock, - uint256 endblock, bytes calldata extraParams ) external returns (uint256 registeredProposalId); - // TODO: what's most proper way to update the voting period? - // TODO: do we want to include cancel or withdraw? - // TODO: what's the best way to include weight scheme? + function executeProposal(uint256 proposalId, bytes calldata extraParams) external; } ``` @@ -71,9 +69,10 @@ interface IERC5247SmartProposal { * Arrays were used for `target`s, `value`s, `calldata`s instead of single variables, allowing a proposal to carry arbitrarily long multiple functional calls. * `registeredProposalId` is returned in `createProposal` so the standard can support implementation to decide their own format of proposal id. - ## Security Considerations + Needs discussion. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 6fa810162f2d3347288d9269e8a3fdc2fc6a2512 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 8 Dec 2022 21:48:49 -0800 Subject: [PATCH 024/274] Update EIP-5247 add assets (#6109) * Update EIP * Add refimpl --- EIPS/eip-5247.md | 69 ++++++++++ assets/eip-5247/ProposalRegistry.sol | 65 ++++++++++ assets/eip-5247/testProposalRegistry.ts | 162 ++++++++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 assets/eip-5247/ProposalRegistry.sol create mode 100644 assets/eip-5247/testProposalRegistry.ts diff --git a/EIPS/eip-5247.md b/EIPS/eip-5247.md index 8d2823c9737a4d..bb7779d680fa84 100644 --- a/EIPS/eip-5247.md +++ b/EIPS/eip-5247.md @@ -69,6 +69,75 @@ interface IERC5247 { * Arrays were used for `target`s, `value`s, `calldata`s instead of single variables, allowing a proposal to carry arbitrarily long multiple functional calls. * `registeredProposalId` is returned in `createProposal` so the standard can support implementation to decide their own format of proposal id. +## Test Cases + +A simple test case can be found as + +```ts + it("Should work for a simple case", async function () { + const { contract, erc721, owner } = await loadFixture(deployFixture); + const callData1 = erc721.interface.encodeFunctionData("mint", [owner.address, 1]); + const callData2 = erc721.interface.encodeFunctionData("mint", [owner.address, 2]); + await contract.connect(owner) + .createProposal( + 0, + [erc721.address, erc721.address], + [0,0], + [0,0], + [callData1, callData2], + []); + expect(await erc721.balanceOf(owner.address)).to.equal(0); + await contract.connect(owner).executeProposal(0, []); + expect(await erc721.balanceOf(owner.address)).to.equal(2); + }); +``` + +See [testProposalRegistry.ts](../assets/eip-5247/testProposalRegistry.ts) for the whole testset. + +## Reference Implementation + +A simple reference implementation can be found. + +```solidity + function createProposal( + uint256 proposalId, + address[] calldata targets, + uint256[] calldata values, + uint256[] calldata gasLimits, + bytes[] calldata calldatas, + bytes calldata extraParams + ) external returns (uint256 registeredProposalId) { + require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch"); + require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch"); + require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch"); + registeredProposalId = proposalCount; + proposalCount++; + + proposals[registeredProposalId] = Proposal({ + by: msg.sender, + proposalId: proposalId, + targets: targets, + values: values, + calldatas: calldatas, + gasLimits: gasLimits + }); + emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams); + return registeredProposalId; + } + function executeProposal(uint256 proposalId, bytes calldata extraParams) external { + Proposal storage proposal = proposals[proposalId]; + address[] memory targets = proposal.targets; + string memory errorMessage = "Governor: call reverted without message"; + for (uint256 i = 0; i < targets.length; ++i) { + (bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]); + Address.verifyCallResult(success, returndata, errorMessage); + } + emit ProposalExecuted(msg.sender, proposalId, extraParams); + } +``` + +See [ProposalRegistry.sol](../assets/eip-5247/ProposalRegistry.sol) for more information. + ## Security Considerations Needs discussion. diff --git a/assets/eip-5247/ProposalRegistry.sol b/assets/eip-5247/ProposalRegistry.sol new file mode 100644 index 00000000000000..76f56f2c99be98 --- /dev/null +++ b/assets/eip-5247/ProposalRegistry.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +// A fully runnalbe version can be found in https://github.com/ercref/ercref-contracts/tree/869843f23dc4da793f0d9d018ed92e3950da8f75 +pragma solidity ^0.8.17; + +import "./IERC5247.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +struct Proposal { + address by; + uint256 proposalId; + address[] targets; + uint256[] values; + uint256[] gasLimits; + bytes[] calldatas; +} + +contract ProposalRegistry is IERC5247 { + using Address for address; + mapping(uint256 => Proposal) public proposals; + uint256 private proposalCount; + function createProposal( + uint256 proposalId, + address[] calldata targets, + uint256[] calldata values, + uint256[] calldata gasLimits, + bytes[] calldata calldatas, + bytes calldata extraParams + ) external returns (uint256 registeredProposalId) { + require(targets.length == values.length, "GeneralForwarder: targets and values length mismatch"); + require(targets.length == gasLimits.length, "GeneralForwarder: targets and gasLimits length mismatch"); + require(targets.length == calldatas.length, "GeneralForwarder: targets and calldatas length mismatch"); + registeredProposalId = proposalCount; + proposalCount++; + + proposals[registeredProposalId] = Proposal({ + by: msg.sender, + proposalId: proposalId, + targets: targets, + values: values, + calldatas: calldatas, + gasLimits: gasLimits + }); + emit ProposalCreated(msg.sender, proposalId, targets, values, gasLimits, calldatas, extraParams); + return registeredProposalId; + } + function executeProposal(uint256 proposalId, bytes calldata extraParams) external { + Proposal storage proposal = proposals[proposalId]; + address[] memory targets = proposal.targets; + string memory errorMessage = "Governor: call reverted without message"; + for (uint256 i = 0; i < targets.length; ++i) { + (bool success, bytes memory returndata) = proposal.targets[i].call{value: proposal.values[i]}(proposal.calldatas[i]); + Address.verifyCallResult(success, returndata, errorMessage); + } + emit ProposalExecuted(msg.sender, proposalId, extraParams); + } + + function getProposal(uint256 proposalId) external view returns (Proposal memory) { + return proposals[proposalId]; + } + + function getProposalCount() external view returns (uint256) { + return proposalCount; + } + +} diff --git a/assets/eip-5247/testProposalRegistry.ts b/assets/eip-5247/testProposalRegistry.ts new file mode 100644 index 00000000000000..947c40bebc327b --- /dev/null +++ b/assets/eip-5247/testProposalRegistry.ts @@ -0,0 +1,162 @@ +// A fully runnalbe version can be found in https://github.com/ercref/ercref-contracts/tree/869843f23dc4da793f0d9d018ed92e3950da8f75 +import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { hexlify } from "ethers/lib/utils"; +import { ethers } from "hardhat"; + +describe("ProposalRegistry", function () { + async function deployFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, otherAccount] = await ethers.getSigners(); + + const ProposalRegistry = await ethers.getContractFactory("ProposalRegistry"); + const contract = await ProposalRegistry.deploy(); + + const ERC721ForTesting = await ethers.getContractFactory("ERC721ForTesting"); + const erc721 = await ERC721ForTesting.deploy(); + + const SimpleForwarder = await ethers.getContractFactory("SimpleForwarder"); + const forwarder = await SimpleForwarder.deploy(); + return { contract, erc721, forwarder, owner, otherAccount }; + } + + describe("Deployment", function () { + it("Should work for a simple case", async function () { + const { contract, erc721, owner } = await loadFixture(deployFixture); + const callData1 = erc721.interface.encodeFunctionData("mint", [owner.address, 1]); + const callData2 = erc721.interface.encodeFunctionData("mint", [owner.address, 2]); + await contract.connect(owner) + .createProposal( + 0, + [erc721.address, erc721.address], + [0,0], + [0,0], + [callData1, callData2], + []); + expect(await erc721.balanceOf(owner.address)).to.equal(0); + await contract.connect(owner).executeProposal(0, []); + expect(await erc721.balanceOf(owner.address)).to.equal(2); + }); + const Ns = [0, 50, 100, 150, 200]; + for (let n of Ns) { + + it(`Should work for a proposal case of ${n}`, async function () { + const { contract, erc721, owner } = await loadFixture(deployFixture); + const numOfMint = n; + const calldatas = []; + for (let i = 0 ; i < numOfMint; i++) { + const callData = erc721.interface.encodeFunctionData("mint", [owner.address, i]); + calldatas.push(callData); + } + let txCreate = await contract.connect(owner) + .createProposal( + 0, + Array(numOfMint).fill(erc721.address), + Array(numOfMint).fill(0), + Array(numOfMint).fill(0), + calldatas, + []); + let txCreateWaited = await txCreate.wait(); + console.log(`Creation TX gas`, txCreateWaited.cumulativeGasUsed.toString()); + console.log(`Gas per mint`, parseInt(txCreateWaited.cumulativeGasUsed.toString()) / numOfMint); + expect(await erc721.balanceOf(owner.address)).to.equal(0); + let txExecute = await contract.connect(owner).executeProposal(0, []); + let txExecuteWaited = await txExecute.wait(); + console.log(`Execution TX gas`, txExecuteWaited.cumulativeGasUsed.toString()); + console.log(`Gas per mint`, parseInt(txExecuteWaited.cumulativeGasUsed.toString()) / numOfMint); + expect(await erc721.balanceOf(owner.address)).to.equal(numOfMint); + }); + } + }); + describe("Benchmark", function () { + it(`Should work for a forwarding case`, async function () { + const { forwarder, erc721, owner } = await loadFixture(deployFixture); + const numOfMint = 200; + const calldatas = []; + for (let i = 0 ; i < numOfMint; i++) { + const callData = erc721.interface.encodeFunctionData("mint", [owner.address, i]); + calldatas.push(callData); + } + expect(await erc721.balanceOf(owner.address)).to.equal(0); + let txForward = await forwarder.connect(owner) + .forward( + Array(numOfMint).fill(erc721.address), + Array(numOfMint).fill(0), + Array(numOfMint).fill(0), + calldatas); + let txForwardWaited = await txForward.wait(); + + console.log(`txForwardWaited TX gas`, txForwardWaited.cumulativeGasUsed.toString()); + + console.log(`Gas per mint`, parseInt(txForwardWaited.cumulativeGasUsed.toString()) / numOfMint); + expect(await erc721.balanceOf(owner.address)).to.equal(numOfMint); + + }); + + + it(`Should work for erc721 batchMint with same addresses`, async function () { + const { erc721, owner } = await loadFixture(deployFixture); + const numOfMint = 200; + const tokenIds = []; + const addresses = []; + + for (let i = 0 ; i < numOfMint; i++) { + addresses.push(owner.address);// addresses.push(hexlify(ethers.utils.randomBytes(20))); + tokenIds.push(i); + } + const tx = await erc721.connect(owner).batchMint(addresses, tokenIds); + const txWaited = await tx.wait(); + console.log(`batchMint TX gas`, txWaited.cumulativeGasUsed.toString()); + console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint); + }) + + it(`Should work for erc721 batchMint with different addresses`, async function () { + const { erc721, owner } = await loadFixture(deployFixture); + const numOfMint = 200; + const tokenIds = []; + const addresses = []; + + for (let i = 0 ; i < numOfMint; i++) { + addresses.push(hexlify(ethers.utils.randomBytes(20))); + tokenIds.push(i); + } + const tx = await erc721.connect(owner).batchMint(addresses, tokenIds); + const txWaited = await tx.wait(); + console.log(`batchMint TX gas`, txWaited.cumulativeGasUsed.toString()); + console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint); + }); + + + it(`Should work for erc721 batchSafeMint with same addresses`, async function () { + const { erc721, owner } = await loadFixture(deployFixture); + const numOfMint = 400; + const tokenIds = []; + const addresses = []; + + for (let i = 0 ; i < numOfMint; i++) { + addresses.push(owner.address);// addresses.push(hexlify(ethers.utils.randomBytes(20))); + tokenIds.push(i); + } + const tx = await erc721.connect(owner).batchSafeMint(addresses, tokenIds); + const txWaited = await tx.wait(); + console.log(`batchSafeMint TX gas`, txWaited.cumulativeGasUsed.toString()); + console.log(`At ${numOfMint} Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint); + }); + + it(`Should work for erc721 batchSafeMint with different addresses`, async function () { + const { erc721, owner } = await loadFixture(deployFixture); + const numOfMint = 400; + const tokenIds = []; + const addresses = []; + + for (let i = 0 ; i < numOfMint; i++) { + addresses.push(hexlify(ethers.utils.randomBytes(20))); + tokenIds.push(i); + } + const tx = await erc721.connect(owner).batchSafeMint(addresses, tokenIds); + const txWaited = await tx.wait(); + console.log(`batchSafeMint TX gas`, txWaited.cumulativeGasUsed.toString()); + console.log(`At ${numOfMint} the Gas per mint`, parseInt(txWaited.cumulativeGasUsed.toString()) / numOfMint); + }); + }); +}); From 2d2e28b8bacd2b069897b38183a05a0dc6223441 Mon Sep 17 00:00:00 2001 From: eth-bot <85952233+eth-bot@users.noreply.github.com> Date: Sat, 10 Dec 2022 16:17:55 -0800 Subject: [PATCH 025/274] (bot 1272989785) moving EIPS/eip-3651.md to stagnant (#6009) PR 6009 with changes to EIPS/eip-3651.md was created on (2022-Nov-20th@00.20.18) which is before the cutoff date of (2022-Nov-27th@00.17.53) i.e. 2 weeks ago --- EIPS/eip-3651.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3651.md b/EIPS/eip-3651.md index 8d64ad9e1f6892..96688016921f23 100644 --- a/EIPS/eip-3651.md +++ b/EIPS/eip-3651.md @@ -4,7 +4,7 @@ title: Warm COINBASE description: Starts the `COINBASE` address warm author: William Morriss (@wjmelements) discussions-to: https://ethereum-magicians.org/t/eip-3651-warm-coinbase/6640 -status: Review +status: Stagnant type: Standards Track category: Core created: 2021-07-12 From 7080e182175d16ba2e429cccef74368d87e3a99d Mon Sep 17 00:00:00 2001 From: frenchkebab <36957058+Frenchkebab@users.noreply.github.com> Date: Tue, 13 Dec 2022 04:00:00 +0900 Subject: [PATCH 026/274] Update eip-1822.md (#6118) Fixes typo: contact -> contract --- EIPS/eip-1822.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-1822.md b/EIPS/eip-1822.md index 590bd3ae622df3..9289266297ba7a 100644 --- a/EIPS/eip-1822.md +++ b/EIPS/eip-1822.md @@ -306,7 +306,7 @@ contract LibraryLock is LibraryLockDataLayout { } } -contact ERC20DataLayout is LibraryLockDataLayout { +contract ERC20DataLayout is LibraryLockDataLayout { uint256 public totalSupply; mapping(address=>uint256) public tokens; } From 767cd4189c1ca8d17ec6e72d0d24ba249d2b7784 Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 13 Dec 2022 01:50:16 +0000 Subject: [PATCH 027/274] Update EIP-5646: Move to Last Call (#6097) * Add EIP-5646: Token state fingerprint * Update EIP-5646: add link to discussion * Update EIP-5646: reference ERC standards as EIP-N * Update EIP-5646: update all EIP references to links * Update EIP-5646: fix incorrect EIP references * Update EIP-5646: Rework the structure of the whole draft * Update EIP-5646: Move to Last Call --- EIPS/eip-5646.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5646.md b/EIPS/eip-5646.md index 90aee881188ccd..d7696e0424d204 100644 --- a/EIPS/eip-5646.md +++ b/EIPS/eip-5646.md @@ -4,7 +4,8 @@ title: Token State Fingerprint description: Unambiguous token state identifier author: Naim Ashhab (@ashhanai) discussions-to: https://ethereum-magicians.org/t/eip-5646-discussion-token-state-fingerprint/10808 -status: Review +status: Last Call +last-call-deadline: 2022-12-21 type: Standards Track category: ERC created: 2022-09-11 From d05f436944b6371a20a613d3f723e74c37f001b4 Mon Sep 17 00:00:00 2001 From: Javier Arcenegui Almenara <81355285+Hardblock-IMSE-CNM@users.noreply.github.com> Date: Tue, 13 Dec 2022 16:38:17 +0100 Subject: [PATCH 028/274] Update EIP-4519: Move to Final (#6076) * Update eip-4519.md * Update eip-4519.md * Update eip-4519.md * Update EIPS/eip-4519.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-4519.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-4519.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-4519.md b/EIPS/eip-4519.md index f12a3a2ece8c29..5382f517ffa547 100644 --- a/EIPS/eip-4519.md +++ b/EIPS/eip-4519.md @@ -36,33 +36,33 @@ Finally, if both the attributes `addressAsset` and `addressUser` are defined, th 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. 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 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 $SK_{O_{-}A}$ and a Public Key $PK_{O_{-}A}$, so that $PK_{O_{-}A} = SK_{O_{-}A} * P$. To generate the shared key between the owner and the asset, $K_O$, the public key of the asset, $PK_A$, is employed as follows: +When the 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: -$$K_O = PK_A*SK_{O_{-}A}$$ +KO = PKA * SKO_A -Using the function `startOwnerEngagement`, $PK_{O_{-}A}$ is saved as the attribute `dataEngagement` and the hash of $K_O$ as the attribute `hashK_OA`. The owner sends request engagement to the asset, and the asset calculates: +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: -$$K_A = SK_A*PK_{O_{-}A}$$ +KA = SKA * PKO_A -If everything is correctly done, $K_O$ and $K_A$ are the same since: +If everything is correctly done, KO and KA are the same since: -$$K_O=PK_A\*SK_{O_{-}A}=(SK_A\*P)\*SK_{O_{-}A}= SK_A\*(SK_{O_{-}A}\*P)=SK_A\*PK_{O_{-}A}$$ +KO = PKA * SKO_A = (SKA * P) * SKO_A = SKA * (SKO_A * P) = SKA * PKO_A -Using the function `ownerEngagement`, the asset sends the hash of $K_A$, and if it is the same as the data in `hashK_OA`, then the state of the token changes to `engagedWithOwner` and the event `OwnerEngaged` are sent. Once the asset receives the event, it changes its operation mode to `engagedWithOwner`. This process is shown in `Figure 4`. From this moment, the asset can be managed by the owner and they can communicate in a secure way using the shared key. +Using the function `ownerEngagement`, the asset sends the hash of KA, and if it is the same as the data in `hashK_OA`, then the state of the token changes to `engagedWithOwner` and the event `OwnerEngaged` are sent. Once the asset receives the event, it changes its operation mode to `engagedWithOwner`. This process is shown in `Figure 4`. From this moment, the asset can be managed by the owner and they can communicate in a secure way using the shared key. ![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 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, $PK_{U_{-}A}$, as the attribute `dataEngagement` and the hash of $K_U = PK_A * SK_{U_{-}A}$ as the attribute `hashK_UA` in the NFT. +If the asset consults Ethereum and the state of its 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 NFT. The user sends request engagement and the asset calculates: -$$K_A = SK_A*PK_{U_{-}A}$$ +KA = SKA*PKU_A -If everything is correctly done, $K_U$ and $K_A$ are the same since: +If everything is correctly done, KU and KA are the same since: -$$K_U=PK_A\*SK_{U_{-}A}=(SK_A\*P)\*SK_{U_{-}A}= SK_A\*(SK_{U_{-}A}\*P)=SK_A\*PK_{U_{-}A}$$ +KU = PKA * SKU_A = (SKA * P) * SKU_A = SKA * (SKU_A * P) = SKA * PKU_A -Using the function `userEngagement`, the asset sends the hash of $K_A$ 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. +Using the function `userEngagement`, the asset sends the hash of KA 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/Figure5.jpg) From 92f437ec43b5578c946b24561e46e762757d27e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Tue, 13 Dec 2022 17:24:14 +0100 Subject: [PATCH 029/274] EIP-4750: Fix formatting (#6125) --- EIPS/eip-4750.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 3ca0e8bf541af8..2da4893dbd9947 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -17,13 +17,13 @@ Introduce the ability to have several code sections in EOF-formatted ([EIP-3540] ## Motivation -Currently in the EVM everything is a dynamic jump. Languages like Solidity generate most jumps in a static manner (i.e. the destination is pushed to the stack right before, `PUSHn .. JUMP`). Unfortunately however this cannot be used by most EVM interpreters, because of added requirement of validation/analysis. This also restricts them from making optimisations and potentially reducing the cost of jumps. +Currently, in the EVM everything is a dynamic jump. Languages like Solidity generate most jumps in a static manner (i.e. the destination is pushed to the stack right before, `PUSHn .. JUMP`). Unfortunately however this cannot be used by most EVM interpreters, because of added requirement of validation/analysis. This also restricts them from making optimisations and potentially reducing the cost of jumps. [EIP-4200](./eip-4200.md) introduces static jump instructions, which remove the need for *most* dynamic jump use cases, but not everything can be solved with them. This EIP aims to remove the need and disallow dynamic jumps as it offers the most important feature those are used for: calling into and returning from functions. -Furthermore it aims to improve analysis opportunities by encoding the number of inputs and outputs for each given function, and isolating the stack of each function (i.e. a function cannot read the stack of the caller/callee). +Furthermore, it aims to improve analysis opportunities by encoding the number of inputs and outputs for each given function, and isolating the stack of each function (i.e. a function cannot read the stack of the caller/callee). ## Specification @@ -36,7 +36,7 @@ Furthermore it aims to improve analysis opportunities by encoding the number of 5. Exactly one type section MAY be present. 6. The type section, if present, MUST directly precede all code sections. 7. The type section, if present, contains a sequence of pairs of bytes: first byte in a pair encodes number of inputs, and second byte encodes number of outputs of the code section with the same index. *Note:* This implies that there is a limit of 256 stack for the input and in the output. -8. Therefore type section size MUST be `n * 2` bytes, where `n` is the number of code sections. +8. Therefore, type section size MUST be `n * 2` bytes, where `n` is the number of code sections. 9. First code section MUST have 0 inputs and 0 outputs. 10. Type section MAY be omitted if only a single code. section is present. In that case it implicitly defines 0 inputs and 0 outputs for this code section. @@ -66,7 +66,7 @@ We introduce three new instructions: 1. `CALLF` (`0xb0`) - call a function 2. `RETF` (`0xb1`) - return from a function -2. `JUMPF` (`0xb2`) - jump to a function (without updating return stack) +3. `JUMPF` (`0xb2`) - jump to a function (without updating return stack) If the code is legacy bytecode, any of these instructions results in an *exceptional halt*. (*Note: This means no change to behaviour.*) @@ -88,17 +88,17 @@ If the code is valid EOF1, the following execution rules apply: 6. Pops nothing and pushes nothing to data stack. 7. Pushes to return stack an item: -``` -(code_section_index = current_section_index, -offset = PC_post_instruction, -stack_height = data_stack.height - types[code_section_index].inputs) -``` - -Under `PC_post_instruction` we mean the PC position after the entire immediate argument of `CALLF`. Data stack height is saved as it was before function inputs were pushed. - -*Note:* Code validation rules of [EIP-3670](./eip-3670.md) guarantee there is always an instruction following `CALLF` (since terminating instruction is required to be final one in the section), therefore `PC_post_instruction` always points to an instruction inside section bounds. - -7. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. + ``` + (code_section_index = current_section_index, + offset = PC_post_instruction, + stack_height = data_stack.height - types[code_section_index].inputs) + ``` + + Under `PC_post_instruction` we mean the PC position after the entire immediate argument of `CALLF`. Data stack height is saved as it was before function inputs were pushed. + + *Note:* Code validation rules of [EIP-3670](./eip-3670.md) guarantee there is always an instruction following `CALLF` (since terminating instruction is required to be final one in the section), therefore `PC_post_instruction` always points to an instruction inside section bounds. + +8. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. #### `JUMPF` From 4439d0ed095c8b3dd0942e3f71865673a37f96b4 Mon Sep 17 00:00:00 2001 From: Javier Arcenegui Almenara <81355285+Hardblock-IMSE-CNM@users.noreply.github.com> Date: Tue, 13 Dec 2022 17:27:40 +0100 Subject: [PATCH 030/274] Update EIP-4519: Move to Final (#6124) * Update eip-4519.md * Update eip-4519.md * Update eip-4519.md * Update EIPS/eip-4519.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-4519.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Update eip-4519.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-4519.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4519.md b/EIPS/eip-4519.md index 5382f517ffa547..b1743323f289e4 100644 --- a/EIPS/eip-4519.md +++ b/EIPS/eip-4519.md @@ -4,8 +4,7 @@ title: Non-Fungible Tokens Tied to Physical Assets description: Interface for non-fungible tokens representing physical assets that can generate or recover their own accounts and obey users. author: Javier Arcenegui (@Hardblock-IMSE-CNM), Rosario Arjona (@RosarioArjona), Roberto Román , Iluminada Baturone (@lumi2018) discussions-to: https://ethereum-magicians.org/t/new-proposal-of-smart-non-fungible-token/7677 -status: Last Call -last-call-deadline: 2022-11-30 +status: Final type: Standards Track category: ERC created: 2021-12-03 @@ -56,7 +55,7 @@ If the asset consults Ethereum and the state of its NFT is `waitingForUser`, the The user sends request engagement and the asset calculates: -KA = SKA*PKU_A +KA = SKA * PKU_A If everything is correctly done, KU and KA are the same since: From ce8e5d1cd4de1acee546250fafa9d18cb038e98e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Tue, 13 Dec 2022 17:28:50 +0100 Subject: [PATCH 031/274] EIP-4200: Do not suggest PC is deprecated (#6111) --- EIPS/eip-4200.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index 23080249f36671..c6a311a0ee071d 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -61,11 +61,9 @@ Because the destinations are validated upfront, the cost of these instructions a ### Relative addressing -We chose relative addressing in order to support code which is moveable. This also means it can be injected. A technique seen used prior to this EIP to achieve this same goal was to inject code like `PUSHn PC ADD JUMPI`. +We chose relative addressing in order to support code which is relocatable. This also means a code snippet can be injected. A technique seen used prior to this EIP to achieve the same goal was to inject code like `PUSHn PC ADD JUMPI`. -We do not see any significant downside to relative addressing, but it also allows the deprecation of the `PC` instruction. - -*Note: EIP-3670 should reject `PC`.* +We do not see any significant downside to relative addressing, but it also opens possibility to the deprecation of the `PC` instruction. ### Immediate size From b4db14cf02d820f7bf417d2d1d0043573106d4d4 Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Tue, 13 Dec 2022 12:00:08 -0500 Subject: [PATCH 032/274] Pickup latest eipw version (#6091) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00b9a8247d6756..1f4985a0b19856 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - uses: ethereum/eipw-action@d57d2afb71253fc7447d8a7a28d60f9dead2290b + - uses: ethereum/eipw-action@859c779fd8ee3ae73f8b1aa9b69d87ea621074fe id: eipw with: token: ${{ secrets.GITHUB_TOKEN }} From 71b3a7d7ff79aa7c3eb9f3b53e27fed0c17dc986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 13 Dec 2022 13:27:36 -0500 Subject: [PATCH 033/274] Add EIP-6093: Custom errors for commonly-used tokens (#6093) * EIP- : Custom errors for ERC tokens * Update EIP number, discussion and created * Added EIP number to file name * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6093.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Added `commonly-used` term Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6093.md | 371 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 371 insertions(+) create mode 100644 EIPS/eip-6093.md diff --git a/EIPS/eip-6093.md b/EIPS/eip-6093.md new file mode 100644 index 00000000000000..2f85e9aeb7858f --- /dev/null +++ b/EIPS/eip-6093.md @@ -0,0 +1,371 @@ +--- +eip: 6093 +title: Custom errors for commonly-used tokens +description: Lists custom errors for common token implementations +author: Ernesto García (@ernestognw), Francisco Giordano (@frangio), Hadrien Croubois (@Amxx) +discussions-to: https://ethereum-magicians.org/t/eip-6093-custom-errors-for-erc-tokens/12043 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-06 +requires: 20, 721, 1155 +--- + +## Abstract + +This EIP defines a standard set of custom errors for commonly-used tokens, which are defined as [EIP-20](./eip-20.md), [EIP-721](./eip-721.md), and [EIP-1155](./eip-1155.md) tokens. + +Ethereum applications and wallets have historically relied on revert reason strings to display the cause of transaction errors to users. More recent Solidity versions offer rich revert reasons with error-specific decoding (sometimes referred to as "custom errors"). This EIP defines a standard set of errors designed to give at least the same relevant information as revert reason strings, but in a structured and expected way that clients can implement decoding for. + +## Motivation + +Since the introduction of Solidity custom errors in v0.8.4, these have provided a way to show failures in a more expresive and gas efficient manner with dynamic arguments, while reducing deployment costs. + +However, [EIP-20](./eip-20.md), [EIP-721](./eip-721.md), [EIP-1155](./eip-1155.md) were already finalized when custom errors were released, so no errors are included in their specification. + +Standardized errors allow users to expect more consistent error messages across applications or testing environments, while exposing pertinent arguments and overall reducing the need of writing expensive revert strings in the deployment bytecode. + +## Specification + +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 following errors were designed according to the criteria described in [Rationale](#rationale). + +This EIP defines standard errors that may be used by implementations in certain scenarios, but does not specify whether implementations should revert in those scenarios, which remains up to the implementers, unless a revert is mandated by the corresponding EIPs. + +The names of the error arguments are defined in the [Parameter Glossary](#parameter-glossary), and MUST be used according to those definitions. + +### [EIP-20](./eip-20.md) + +#### `ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)` + +Indicates an error related to the current `balance` of a `sender`. +Used in transfers. + +- MUST be used when `balance` is less than `needed`. +- MUST NOT be used if `balance` is equal or greater than `needed`. + +#### `ERC20InvalidSender(address sender)` + +Indicates a failure with the token `sender`. +Used in transfers. + +- MUST be used for disallowed transfers from the zero address. +- MUST NOT be used for approval operations. +- MUST NOT be used for balance or allowance requirements. + - Use `ERC20InsufficientBalance` or `ERC20InsufficientAllowance` instead. + +#### `ERC20InvalidReceiver(address receiver)` + +Indicates a failure with the token `receiver`. +Used in transfers. + +- MUST be used for disallowed transfers to the zero address. +- MUST be used for disallowed transfers to non-compatible addresses (eg. contract addresses). +- MUST NOT be used for approval operations. + +#### `ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)` + +Indicates a failure with the `spender`'s `allowance`. +Used in transfers. + +- MUST be used when `allowance` is less than `needed`. +- MUST NOT be used if `allowance` is equal or greater than `needed`. + +#### `ERC20InvalidApprover(address approver)` + +Indicates a failure with the `approver` of a token to be approved. +Used in approvals. + +- MUST be used for disallowed approvals from the zero address. +- MUST NOT be used for transfer operations. + +#### `ERC20InvalidSpender(address spender)` + +Indicates a failure with the `spender` to be approved. +Used in approvals. + +- MUST be used for disallowed approvals to the zero address. +- MUST be used for disallowed approvals to the owner itself. +- MUST NOT be used for transfer operations. + - Use `ERC20InsufficientAllowance` instead. + +### [EIP-721](./eip-721.md) + +#### `ERC721InvalidOwner(address sender, uint256 tokenId, address owner)` + +Indicates an error related to the ownership over a particular token. +Used in transfers. + +- MUST be used when `sender` is not `owner`. +- MUST NOT be used for approval operations. + +#### `ERC721InvalidSender(address sender)` + +Indicates a failure with the token sender. +Used in transfers. + +- MUST be used for disallowed transfers from the zero address. +- MUST NOT be used for approval operations. +- MUST NOT be used for ownership or approval requirements. + - Use `ERC721InvalidOwner` or `ERC721InsufficientApproval` instead. + +#### `ERC721InvalidReceiver(address receiver)` + +Indicates a failure with the token receiver. +Used in transfers. + +- MUST be used for disallowed transfers to the zero address. +- MUST be used for disallowed transfers to non-`ERC721TokenReceiver` contracts or those that reject a transfer. (eg. returning an invalid response in `onERC721Received`). +- MUST NOT be used for approval operations. + +#### `ERC721InsufficientApproval(address operator, uint256 tokenId)` + +Indicates a failure with the `operator`'s approval. +Used in transfers. + +- MUST be used when operator `isApprovedForAll(owner, operator)` is false. +- MUST be used when operator `getApproved(tokenId)` is not `operator`. + +#### `ERC721InvalidApprover(address approver)` + +Indicates a failure with the `owner` of a token to be approved. +Used in approvals. + +- MUST be used for disallowed approvals from the zero address. +- MUST NOT be used for transfer operations. + +#### `ERC721InvalidOperator(address operator)` + +Indicates a failure with the `operator` to be approved. +Used in approvals. + +- MUST be used for disallowed approvals to the zero address. +- MUST be used for disallowed approvals to the owner itself. +- MUST NOT be used for transfer operations. + - Use `ERC721InsufficientApproval` instead. + +### [EIP-1155](./eip-1155.md) + +#### `ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId)` + +Indicates an error related to the current `balance` of a sender. +Used in transfers. + +- MUST be used when `balance` is less than `needed` for a `tokenId`. +- MUST NOT be used if `balance` is equal or greater than `needed` for an `tokenId`. + +#### `ERC1155InvalidSender(address sender)` + +Indicates a failure with the token sender. +Used in transfers. + +- MUST be used for disallowed transfers from the zero address. +- MUST NOT be used for approval operations. +- MUST NOT be used for balance or allowance requirements. + - Use `ERC1155InsufficientBalance` or `ERC1155InsufficientApproval` instead. + +#### `ERC1155InvalidReceiver(address receiver)` + +Indicates a failure with the token receiver. +Used in transfers. + +- MUST be used for disallowed transfers to the zero address. +- MUST be used for disallowed transfers to non-`ERC1155TokenReceiver` contracts or those that reject a transfer. (eg. returning an invalid response in `onERC1155Received`). +- MUST NOT be used for approval operations. + +#### `ERC1155InsufficientApproval(address operator, uint256 tokenId)` + +Indicates a failure with the `operator`'s approval in a transfer. +Used in transfers. + +- MUST be used when operator `isApprovedForAll(owner, operator, tokenId)` is false. + +#### `ERC1155InvalidApprover(address approver)` + +Indicates a failure with the `approver` of a token to be approved. +Used in approvals. + +- MUST be used for disallowed approvals from the zero address. +- MUST NOT be used for transfer operations. + +#### `ERC1155InvalidOperator(address operator)` + +Indicates a failure with the `operator` to be approved. +Used in approvals. + +- MUST be used for disallowed approvals to the zero address. +- MUST be used for disallowed approvals to the owner itself. +- MUST NOT be used for transfer operations. + - Use `ERC1155InsufficientApproval` instead. + +#### `ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength)` + +Indicates an array length mismatch between `ids` and `values` in a `safeBatchTransferFrom` operation. +Used in batch transfers. + +- MUST be used only if `idsLength` is different to `valuesLength` + +### Parameter Glossary + +| Name | Description | +| ----------- | --------------------------------------------------------------------------- | +| `sender` | Address whose tokens are being transferred. | +| `balance` | Current balance for the interacting account. | +| `needed` | Minimum amount required to perform an action. | +| `receiver` | Address to which tokens are being transferred. | +| `spender` | Address that may be allowed to operate on tokens without being their owner. | +| `allowance` | Amount of tokens a `spender` is allowed to operate with. | +| `approver` | Address initiating an approval operation. | +| `tokenId` | Identifier number of a token. | +| `owner` | Address of the current owner of a token. | +| `operator` | Same as `spender`. | +| `*Length` | Array length for the prefixed parameter. | + +### Error additions + +Any addition to this EIP or implementation-specific errors (such as extensions) SHOULD follow the guidelines presented in the [rationale](#rationale) section to keep consistency. + +## Rationale + +The chosen objectives for a standard for token errors are to provide context about the error, and to make moderate use of meaningful arguments (to maintain the code size benefits with respect to strings). + +Considering this, the error names are designed following a basic grammatical structure based on the standard actions that can be performed on each token and the [subjects](#actions-and-subjects) involved. + +### Actions and subjects + +The main actions that can be performed within a token are: + +- **Transfer**: An operation in which a _sender_ moves to a _receiver_ any number of tokens (fungible _balance_ and/or non-fungible _token ids_). +- **Approval**: An operation in which an _approver_ grants any form of _approval_ to an _operator_. + +The subjects outlined above are expected to exhaustively represent _what_ can go wrong in a token transaction, deriving a specific error by adding an [error prefix](#error-prefixes). + +Note that the action is never seen as the subject of an error. Additionally the token itself is not seen as the subject of an error but rather the context in which it happens, as identified in the domain. + +If a subject is called different on a particular token standard, the error should be consistent with the standard's naming convention. + +### Error prefixes + +An error prefix is added to a subject to derive a concrete error condition. +Developers can think about an error prefix as the _why_ an error happened. + +A prefix can be `Invalid` for general incorrectness, or more specific like `Insufficient` for amounts. + +### Domain + +Each error's arguments may vary depending on the token domain. If there are errors with the same name and different arguments, the Solidity compiler currently fails with a `DeclarationError`. + +An example of this is: + +```solidity +InsufficientApproval(address spender, uint256 allowance, uint256 needed); +InsufficientApproval(address operator, uint256 tokenId); +``` + +For that reason, a domain prefix is proposed to avoid declaration clashing, which is the name of the ERC and its corresponding number appended at the beginning. + +Example: + +```solidity +ERC20InsufficientApproval(address spender, uint256 allowance, uint256 needed); +ERC721InsufficientApproval(address operator, uint256 tokenId); +``` + +### Arguments + +The selection of arguments depends on the subject involved, and it should follow the order presented below: + +1. _Who_ is involved with the error (eg. `address sender`) +2. _What_ failed (eg. `uint256 allowance`) +3. _Why_ it failed, expressed in additional arguments (eg. `uint256 needed`) + +A particular argument may fall in overlapping categories (eg. _Who_ may also be _What_), so not all of these will be present but the order shouldn't be broken. + +Some tokens may need a `tokenId`. This is suggested to include at the end as additional information instead of as a subject. + +### Error grammar rules + +Given the above, we can summarize the construction of error names with a grammar that errors will follow: + +``` +(); +``` + +Where: + +- _Domain_: `ERC20`, `ERC721` or `ERC1155`. Although other token standards may be suggested if not considered in this EIP. +- _ErrorPrefix_: `Invalid`, `Insufficient`, or another if it's more appropiated. +- _Subject_: `Sender`, `Receiver`, `Balance`, `Approver`, `Operator`, `Approval` or another if it's more appropiated, and must make adjustments based on domain's naming convention. +- _Arguments_: Follow the [_who_, _what_ and _why_ order](#arguments). + +## Backwards Compatibility + +Tokens already deployed rely mostly on revert strings and make use of `require` instead of custom errors. Even most of the new deployed tokens since Solidity's v0.8.4 release inherit from implementations using revert strings. + +This EIP can not be enforced on non-upgradeable already deployed tokens, however, these tokens generally use similar conventions with small variations such as: + +- including/removing the [domain](#domain). +- using different [error prefixes](#error-prefixes). +- including similar [subjects](#actions-and-subjects). +- changing the grammar order. + +Upgradeable contracts MAY be upgraded to implement this EIP. + +Implementers and DApp developers that implement special support for tokens that are compliant with this EIP, SHOULD tolerate different errors emitted by non-compliant contracts, as well as classic revert strings. + +## Reference Implementation + +### Solidity + +```solidity +pragma solidity ^0.8.4; + +/// @title Standard ERC20 Errors +/// @dev See https://eips.ethereum.org/EIPS/eip-20 +/// https://eips.ethereum.org/EIPS/eip-6093 +interface ERC20Errors { + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + error ERC20InvalidSender(address sender); + error ERC20InvalidReceiver(address receiver); + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + error ERC20InvalidApprover(address approver); + error ERC20InvalidSpender(address spender); +} + +/// @title Standard ERC721 Errors +/// @dev See https://eips.ethereum.org/EIPS/eip-721 +/// https://eips.ethereum.org/EIPS/eip-6093 +interface ERC721Errors { + error ERC721InvalidOwner(address sender, uint256 tokenId, address owner); + error ERC721InvalidSender(address sender); + error ERC721InvalidReceiver(address receiver); + error ERC721InsufficientApproval(address operator, uint256 tokenId); + error ERC721InvalidApprover(address approver); + error ERC721InvalidOperator(address operator); +} + +/// @title Standard ERC1155 Errors +/// @dev See https://eips.ethereum.org/EIPS/eip-1155 +/// https://eips.ethereum.org/EIPS/eip-6093 +interface ERC1155Errors { + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + error ERC1155InvalidSender(address sender); + error ERC1155InvalidReceiver(address receiver); + error ERC1155InsufficientApproval(address operator, uint256 tokenId); + error ERC1155InvalidApprover(address approver); + error ERC1155InvalidOperator(address operator); + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} +``` + +## Security Considerations + +There are no known signature hash collision for the specified errors. + +Tokens upgraded to implement this EIP may break assumptions in other systems relying on revert strings. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). + From ce57851dcfa0309e25192c29112d1f134ca1404f Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Tue, 13 Dec 2022 14:54:24 -0500 Subject: [PATCH 034/274] Revert "Pickup latest eipw version (#6091)" (#6127) This reverts commit b4db14cf02d820f7bf417d2d1d0043573106d4d4. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f4985a0b19856..00b9a8247d6756 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - uses: ethereum/eipw-action@859c779fd8ee3ae73f8b1aa9b69d87ea621074fe + - uses: ethereum/eipw-action@d57d2afb71253fc7447d8a7a28d60f9dead2290b id: eipw with: token: ${{ secrets.GITHUB_TOKEN }} From 7a9673e0bd7f1f8d7e699fb1271b320d63964dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Tue, 13 Dec 2022 21:01:27 +0100 Subject: [PATCH 035/274] EIP-4200: Remove invalid comment about gas costs (#6126) --- EIPS/eip-4200.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index c6a311a0ee071d..828973d6a6a9e8 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -55,7 +55,7 @@ The immediate encoding of `RJUMPV` is more special: the 8-bit `count` value dete We also extend the validation algorithm of [EIP-3670](./eip-3670.md) to verify that each `RJUMP`/`RJUMPI`/`RJUMPV` has a `relative_offset` pointing to an instruction. This means it cannot point to an immediate data of `PUSHn`/`RJUMP`/`RJUMPI`/`RJUMPV`. It cannot point outside of code bounds. It is allowed to point to a `JUMPDEST`, but is not required to. -Because the destinations are validated upfront, the cost of these instructions are less than their dynamic counterparts: `RJUMP` should cost 2, and `RJUMPI` and `RJUMPV` should cost 4. This is a reduction of 2 gas, compared to `JUMP` and `JUMPI`. +Because the destinations are validated upfront, the cost of these instructions are less than their dynamic counterparts: `RJUMP` should cost 2, and `RJUMPI` and `RJUMPV` should cost 4. ## Rationale From e507bbdcff4acff1eff5dde3679f72d797d4a417 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Wed, 14 Dec 2022 12:44:29 +0100 Subject: [PATCH 036/274] Update EIP-3670: Drop "terminating" instruction restriction (#6053) * Update EIP-3670: Drop "terminating" instruction restriction * Update EIP-3540,3670: Sync execution stop condition * Update EIP-3670: Update reference implementation --- EIPS/eip-3540.md | 4 ++-- EIPS/eip-3670.md | 50 +++++++++++++++--------------------------------- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 5fd10b756f6d58..6ac7732035c7a1 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -35,7 +35,7 @@ A non-exhaustive list of proposed changes which could benefit from this format: - Including a `JUMPDEST`-table (to avoid analysis at execution time) and/or removing `JUMPDEST`s entirely. - Introducing static jumps (with relative addresses) and jump tables, and disallowing dynamic jumps at the same time. -- Requiring code section(s) to be terminated by `STOP`. (Assumptions like this can provide significant speed improvements in interpreters, such as a speed-up of ~7% seen in evmone (ethereum/evmone#295). +- Requiring the execution of a code section ends with a terminating instruction. (Assumptions like this can provide significant speed improvements in interpreters, such as a speed-up of ~7% seen in evmone (ethereum/evmone#295). - Multibyte opcodes without any workarounds. - Representing functions as individual code sections instead of subroutines. - Introducing special sections for different use cases, notably Account Abstraction. @@ -125,7 +125,7 @@ For clarity, the *container* refers to the complete account code, while *code* r 1. `JUMPDEST`-analysis is only run on the *code*. 2. Execution starts at the first byte of the *code*, and `PC` is set to 0. -3. If `PC` goes outside the code section bounds, execution aborts with failure. +3. Execution stops if `PC` goes outside the code section bounds. 4. `PC` returns the current position within the *code*. 5. `JUMP`/`JUMPI` uses an absolute offset within the *code*. 6. `CODECOPY`/`CODESIZE`/`EXTCODECOPY`/`EXTCODESIZE`/`EXTCODEHASH` keeps operating on the entire *container*. diff --git a/EIPS/eip-3670.md b/EIPS/eip-3670.md index 92762beea46017..5ef9b52f780d6e 100644 --- a/EIPS/eip-3670.md +++ b/EIPS/eip-3670.md @@ -44,23 +44,17 @@ The EOF1 format provides following forward compatibility properties: This feature is introduced on the very same block EIP-3540 is enabled, therefore every EOF1-compatible bytecode MUST be validated according to these rules. -Previously deprecated instructions `CALLCODE` (0xf2) and `SELFDESTRUCT` (0xff) are invalid and their opcodes do not have any instruction assigned. +1. Previously deprecated instructions `CALLCODE` (0xf2) and `SELFDESTRUCT` (0xff) are invalid and their opcodes are undefined. -At contract creation time both *initcode* and *code* are iterated instruction-by-instruction (the same process is used to perform "JUMPDEST-analysis"). -Bytecode is deemed invalid if any of these conditions is true: - -- it contains opcodes which are not currently assigned to an instruction (for the sake of assigned instructions, we count `INVALID` (0xfe) as assigned), -- the last opcode (*terminating instruction*) is not `STOP` (0x00),`RETURN` (0xf3), `REVERT` (0xfd) or `INVALID` (0xfe). - -*Notice that due to the requirement of the terminating instruction, it is implicitly stated that truncated instructions (e.g. data portion of a `PUSHn`) are disallowed.* - -*Notice that since EOF1 disallows 0-length code section, a valid contract must contain at least a single byte, which must be a terminating instruction.* +2. At contract creation time *instructions validation* is performed on both *initcode* and *code*. The code is invalid if any of the checks below fails. For each instruction: + 1. Check if the opcode is defined. The `INVALID` (0xfe) is considered defined. + 2. Check if all instructions' immediate bytes are present in the code (code does not end in the middle of instruction). ## Rationale -### Terminating instructions +### Immediate data -An efficient interpreter loop would only need to rely on checking if a terminating instruction has been encountered, and if so stopping execution. Currently, this is not possible in the EVM, because of the lack of requirement for a proper termination as well as allowing for truncated instructions, an interpreter must track and check these various conditions. +Allowing implicit zero immediate data for `PUSH` instructions introduces inefficiencies to EVM implementations without any practical use-case (the value of a `PUSH` instruction at the code end cannot be observed by EVM). This EIP requires all immediate bytes to be explicitly present in the code. ### Rejection of deprecated instructions @@ -83,19 +77,15 @@ Each case should be tested for creation transaction, `CREATE` and `CREATE2`. ### Valid codes - EOF code containing `INVALID` -- EOF codes ending with any of the terminating instructions - EOF code with data section containing bytes that are undefined instructions - Legacy code containing undefined instruction - Legacy code ending with incomplete PUSH instruction -- Legacy code ending with any valid non-terminating instruction ### Invalid codes - EOF code containing undefined instruction - EOF code ending with incomplete `PUSH` instruction - This can include `PUSH` instruction unreachable by execution, e.g. after `STOP` -- EOF code ending with any defined instruction other than terminating ones - - This can include codes where non-terminating instruction is not reachable, e.g. `0030` (`STOP ADDRESS`). ## Reference Implementation @@ -118,38 +108,28 @@ valid_opcodes = [ 0xf0, 0xf1, 0xf3, 0xf4, 0xf5, 0xfa, 0xfd, 0xfe ] -# STOP, RETURN, REVERT, INVALID -terminating_opcodes = [ 0x00, 0xf3, 0xfd, 0xfe ] - -# Only for PUSH1..PUSH32 immediate_sizes = 256 * [0] -for opcode in range(0x60, 0x7f + 1): # PUSH1..PUSH32 - immediate_sizes[opcode] = opcode - 0x60 + 1 +immediate_sizes[0x60:0x7f + 1] = range(1, 32 + 1) # PUSH1..PUSH32 + # Raises ValidationException on invalid code -def validate_code(code: bytes): +def validate_instructions(code: bytes): # Note that EOF1 already asserts this with the code section requirements assert len(code) > 0 - opcode = 0 pos = 0 while pos < len(code): # Ensure the opcode is valid opcode = code[pos] - pos += 1 - if not opcode in valid_opcodes: - raise ValidationException("undefined instruction") + if opcode not in valid_opcodes: + raise ValidationException("undefined opcode") - # Skip immediates - pos += immediate_sizes[opcode] + # Skip immediate data + pos += 1 + immediate_sizes[opcode] - # Ensure last opcode's immediate doesn't go over code end - if pos != len(code): + # Ensure last instruction's immediate doesn't go over code end + if pos != len(code): raise ValidationException("truncated immediate") - - # opcode is the *last opcode* - if not opcode in terminating_opcodes: - raise ValidationException("no terminating instruction") ``` ## Security Considerations From 19d8065c50796918a75f9f8cf39c7e1cf6446121 Mon Sep 17 00:00:00 2001 From: Samuele Marro Date: Wed, 14 Dec 2022 17:38:09 +0100 Subject: [PATCH 037/274] Fixed typo. (#6104) --- EIPS/eip-5375.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5375.md b/EIPS/eip-5375.md index c0cd6cc4d0a580..3eaa5516c7ee1f 100644 --- a/EIPS/eip-5375.md +++ b/EIPS/eip-5375.md @@ -190,7 +190,7 @@ and the content of `metadataFields` is `["name", "description"]`, the content of ```json { "name": "The Holy Hand Grenade of Antioch", - "description": "Throw in the general direction of your favorite rabbit, et voilà" + "description": "Throw in the general direction of your favorite rabbit, et voil\u00E0" } ``` From bb1ec143544a76c1e3230752a67668041ab08ffa Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Wed, 14 Dec 2022 13:04:58 -0500 Subject: [PATCH 038/274] Grab latest eipw... again (#6130) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00b9a8247d6756..b7535193a45372 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,7 +99,7 @@ jobs: - name: Checkout EIP Repository uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - uses: ethereum/eipw-action@d57d2afb71253fc7447d8a7a28d60f9dead2290b + - uses: ethereum/eipw-action@451d4e314183dd83401171908ca784e047122d70 id: eipw with: token: ${{ secrets.GITHUB_TOKEN }} From 99c553eff4c0b926b6e37fdede916511c9201987 Mon Sep 17 00:00:00 2001 From: genkifs Date: Wed, 14 Dec 2022 18:42:12 +0000 Subject: [PATCH 039/274] Add EIP-5850: Complex Numbers stored in Bytes32 types (#5850) * Create 5807: Note discussions-to field not yet set * Create 5807: Note discussions-to field not yet set * Security concerns added * Status reordered * Type after status * Reordering * author * author * Whitespace removal * Update and rename eip-5807.md to eip-5850.md * Update EIPS/eip-5850.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5850.md Language improvements Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Implemented suggestions from Pandapip * Formatting * Fix the code block spacing * Added complex numbers use cases * removed wikipedia links * formatting * Formatting * ERC to EIP * ERC deleted * bytes32 references displayed as code * code tag added * Clarifying that the EIP is number format agnostic Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5850.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 EIPS/eip-5850.md diff --git a/EIPS/eip-5850.md b/EIPS/eip-5850.md new file mode 100644 index 00000000000000..09eed5af674e2c --- /dev/null +++ b/EIPS/eip-5850.md @@ -0,0 +1,77 @@ +--- +eip: 5850 +title: Complex Numbers stored in `bytes32` types +description: Store real and imaginary parts of complex numbers in the least significant and most significant 16 bytes respectively of a `bytes32` type. +author: Paul Edge (@genkifs) +discussions-to: https://ethereum-magicians.org/t/eip-5850-store-real-and-imaginary-parts-of-complex-numbers-in-the-least-significant-and-most-significant-16-bytes-respectively-of-a-bytes32-type/11532 +status: Draft +type: Standards Track +category: ERC +created: 2022-10-29 +--- + +## Abstract + +This EIP proposes a natural way for complex numbers to be stored in and retrieved from the `bytes32` data-type. It splits the storage space exactly in half and, most importantly, assigns the real number part to the least significant 16 bytes and the imaginary number part to the most significant 16 bytes. + +## Motivation + +Complex numbers are an essential tool for many mathematical and scientific calculations. For example, Fourier Transforms, Characteristic functions, AC Circuits and Navier-Stokes equations all require the concept. + +Complex numbers can be represented in many different forms (polynomial, cartesian, polar, exponential). The EIP creates a standard that can accomodate cartesian, polar and exponential formats with example code given for the Cartesian representation, where a complex number is just the pair of real numbers which gives the real and imaginary co-ordinates of the complex number. Equal storage capacity is assigned to both components and the order they appear is explicitly defined. + +Packing complex numbers into a single `bytes32` data object halves storage costs and creates a more natural code object that can be passed around the solidity ecosystem. Existing code may not need to be rewritten for complex numbers. For example, mappings by `bytes32` are common and indexing in the 2D complex plane may improve code legibility. + +Decimal numbers, either fix or floating, are not yet fully supported by Solidity so enforcing similar standards for complex versions is premature. It can be suggested that fixed point methods such as prb-math be used with 18 decimal places, or floating point methods like abdk. However, it should be noted that this EIP supports any decimal number representation so long as it fits inside the 16 bytes space. + +## Specification + +A complex number would be defined as `bytes32` and a cartesian representation would be initalized with the `cnNew` function and converted back with `RealIm`, both given below. + +To create the complex number one would use + +```solidity +function cnNew(int128 _Real, int128 _Imag) public pure returns (bytes32){ + bytes32 Imag32 = bytes16(uint128(_Imag)); + bytes32 Real32 = bytes16(uint128(_Real)); + return (Real32>> 128) | Imag32; +} +``` + +and to convert back + +```solidity +function RealIm(bytes32 _cn) public pure returns (int128 Real, int128 Imag){ + bytes16[2] memory tmp = [bytes16(0), 0]; + assembly { + mstore(tmp, _cn) + mstore(add(tmp, 16), _cn) + } + Imag=int128(uint128(tmp[0])); + Real=int128(uint128(tmp[1])); +} +``` + +## Rationale + +An EIP is required as this proposal defines a complex numbers storage/type standard for multiple apps to use. + +This EIP proposes to package both the real and imaginary within one existing data type, `bytes32`. This allows compact storage without the need for structures and facilitates easy library implementations. The `bytes32` would remain available for existing, non-complex number uses. +Only the split and position of the real & imaginary parts is defined in this EIP. Manipulation of complex numbers (addition, multiplication etc.), number of decimal places and other such topics are left for other EIP discussions. This keeps this EIP more focused and therfore more likely to succeed. + +Defining real numbers in the 16 least-significant bytes allows direct conversion from `uint128` to `bytes32` for positive integers less than 2**127. +Direct conversion back from `bytes32` -> `uint` -> `int` are not recommended as the complex number may contain imaginary parts and/or the real part may be negative. It is better to always use `RealIm` for separating the complex part. + +Libraries for complex number manipulation can be implemented with the `Using Complex for bytes32` syntax where `Complex` would be the name of the library. + +## Backwards Compatibility + +There is no impact on other uses of the `bytes32` datatype. + +## Security Considerations + +If complex numbers are manipulated in `bytes32` form then overflow checks must be performed manually during the manipulation. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From d05cb7db98974143657f72a25192bdf201f34371 Mon Sep 17 00:00:00 2001 From: Daniel Tedesco Date: Thu, 15 Dec 2022 02:48:22 +0800 Subject: [PATCH 040/274] Update EIP-4974: Community Feedback (#5135) * Apply suggestions from initial review co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> update links Apply initial suggestions from code review Update EIP number and typos. Co-authored-by: William Schwab <31592931+wschwab@users.noreply.github.com> Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> rename, add enumerable, update language update name conventions, ERC165 identifier update backwards compatability * minor wording edit * update with participate and transfer nomenclature * update reference links * update reference to assets folder * removing the assets link * small update * begin update to rating * Apply suggestions from code review * Full update to Ratings * Fix links * Update interface and add implementation example * Update EIPS/eip-4974.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Update EIPS/eip-4974.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Remove self-references * Fix bot errors * Last bot errors Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-4974.md | 178 ++++++++++++---------------- assets/eip-4974/ERC4974.sol | 222 ++++++++--------------------------- assets/eip-4974/IERC4974.sol | 78 ++++++------ 3 files changed, 162 insertions(+), 316 deletions(-) diff --git a/EIPS/eip-4974.md b/EIPS/eip-4974.md index 834fde7917dd4c..4334f4a86c6ce4 100644 --- a/EIPS/eip-4974.md +++ b/EIPS/eip-4974.md @@ -1,7 +1,7 @@ --- eip: 4974 -title: Experience (EXP) Token Standard -description: A standard interface for fungible, non-tradable tokens, also known as EXP. +title: Ratings +description: An interface for assigning and managing numerical ratings author: Daniel Tedesco (@dtedesco1) discussions-to: https://ethereum-magicians.org/t/8805 status: Draft @@ -12,58 +12,56 @@ requires: 165 --- ## Abstract -The following describes a standard interface for fungible non-tradable tokens, or EXP. This standard provides basic functionality for participant addresses to consent to receive tokens and for an operator address to transfer tokens. -EXP, shorthand for "experience points", may represent accumulated recognition within a smart contract. Like experience points in video games, citations on an academic paper, or Reddit Karma, EXP is bestowed for useful contributions, accumulates as indistinguishable units, and should only be reallocated or destroyed by a reliable authority so empowered. - -The standard described here allows reputation earned to be codified within a smart contract and recognized by other applications: from a five-member local bicycle club to a million-member green energy DAO. +This standard defines a standardized interface for assigning and managing numerical ratings on the Ethereum blockchain. This allows ratings to be codified within smart contracts and recognized by other applications, enabling a wide range of new use cases for tokens. ## Motivation -How reputation manifests across groups can vary widely. Healthy communities allocate reputation to their participants using three key principles: -1. Consent -- No one is forced to be part of the group, but joining requires abiding by the governance structure of the group. -2. Meritocracy -- Reputation is earned by recognition from the group. It cannot be claimed, purchased, or sold. -3. Ethics -- The group can decrease an individual's reputation after bad behavior. -Since the creation of Bitcoin in 2008, the vast majority of blockchain applications have centered on buying and selling digital assets. While these use cases are substantial, digital assets need not be created with trading in mind. In fact, trading can be detrimental to community-based blockchain projects. This was evident in the pay-to-play dynamics of many EVM-based games and DAOs in 2021. +Traditionally, blockchain applications have focused on buying and selling digital assets. However, the asset-centric model has often been detrimental to community-based blockchain projects, as seen in the pay-to-play dynamics of many EVM-based games and DAOs in 2021. + +This proposal addresses this issue by allowing ratings to be assigned to contracts and wallets, providing a new composable primitive for blockchain applications. This allows for a diverse array of new use cases, such as: + +- Voting weight in a DAO: Ratings assigned using this standard can be used to determine the voting weight of members in a decentralized autonomous organization (DAO). For example, a DAO may assign higher ratings to members who have demonstrated a strong track record of contributing to the community, and use these ratings to determine the relative influence of each member in decision-making processes. + +- Experience points in a decentralized game ecosystem: Ratings can be used to track the progress of players in a decentralized game ecosystem, and to reward them for achieving specific milestones or objectives. For example, a game may use ratings to assign experience points to players, which can be used to unlock new content or abilities within the game. -A smart contract cannot directly imbue consent, meritocracy, and ethics into a community, but it can encourage those principles. In doing so, the standard set out below will hopefully unlock a diverse array of new use cases for tokens: -- Voting weight in a DAO -- Experience points in a decentralized game -- Loyalty points for customers of a business +- Loyalty points for customers of a business: Ratings can be used to track the loyalty of customers to a particular business or service, and to reward them for their continued support. For example, a business may use ratings to assign loyalty points to customers, which can be redeemed for special offers or discounts. -This standard is influenced by the [ERC-20](./eip-20) and [ERC-721](./eip-721) token standards and takes cues from each in its structure, style, and semantics. Neither, however, was created for fungible operator-managed token contracts such as EXP. Nor do existing proposals for non-tradable tokens meet the requirements of EXP use cases. +- Asset ratings for a decentralized insurance company: Ratings can be used to evaluate the risk profile of assets in a decentralized insurance company, and to determine the premiums and coverage offered to policyholders. For example, a decentralized insurance company may use ratings to assess the risk of different types of assets, and to provide lower premiums and higher coverage to assets with lower risk ratings. + +This standard is influenced by the [EIP-20](./eip-20.md) and [EIP-721](./eip-721.md) token standards and takes cues from each in its structure, style, and semantics. ## 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. -Every ERC-4974 compliant contract MUST implement the ERC4974 and ERC165 interfaces: +Every compliant contract MUST implement the following interfaces: ``` // SPDX-License-Identifier: CC0 pragma solidity ^0.8.0; -/// @title ERC-4974 Experience (EXP) Token Standard +/// @title EIP-4974 Ratings /// @dev See https://eips.ethereum.org/EIPS/EIP-4974 -/// Note: the ERC-165 identifier for this interface is 0x696e7752. +/// Note: the EIP-165 identifier for this interface is #######. /// Must initialize contracts with an `operator` address that is not `address(0)`. -/// Must initialize contracts assigning participation as `true` for both `operator` and `address(0)`. interface IERC4974 /* is ERC165 */ { /// @dev Emits when operator changes. /// MUST emit when `operator` changes by any mechanism. /// MUST ONLY emit by `setOperator`. - event Appointment(address indexed _operator); + event NewOperator(address indexed _operator); - /// @dev Emits when an address activates or deactivates its participation. - /// MUST emit emit when participation status changes by any mechanism. - /// MUST ONLY emit by `setParticipation`. - event Participation(address indexed _participant, bool _participation); + /// @dev Emits when operator issues a rating. + /// MUST emit when rating is assigned by any mechanism. + /// MUST ONLY emit by `rate`. + event Rating(address _rated, int8 _rating); - /// @dev Emits when operator transfers EXP. - /// MUST emit when EXP is transferred by any mechanism. - /// MUST ONLY emit by `transfer`. - event Transfer(address indexed _from, address indexed _to, uint256 _amount); + /// @dev Emits when operator removes a rating. + /// MUST emit when rating is removed by any mechanism. + /// MUST ONLY emit by `remove`. + event Removal(address _removed); /// @notice Appoint operator authority. /// @dev MUST throw unless `msg.sender` is `operator`. @@ -73,117 +71,91 @@ interface IERC4974 /* is ERC165 */ { /// @param _operator New operator of the smart contract. function setOperator(address _operator) external; - /// @notice Activate or deactivate participation. CALLER IS RESPONSIBLE TO - /// UNDERSTAND THE TERMS OF THEIR PARTICIPATION. - /// @dev MUST throw unless `msg.sender` is `participant`. - /// MUST throw if `participant` is `operator` or zero address. - /// MUST emit a `Participation` event for status changes. - /// @param _participant Address opting in or out of participation. - /// @param _participation Participation status of _participant. - function setParticipation(address _participant, bool _participation) external; + /// @notice Rate an address. + /// MUST emit a Rating event with each successful call. + /// @param _rated Address to be rated. + /// @param _rating Total EXP tokens to reallocate. + function rate(address _rated, int8 _rating) external; - /// @notice Transfer EXP from one address to a participating address. - /// @dev MUST throw unless `msg.sender` is `operator`. - /// MUST throw unless `to` address is participating. - /// MUST throw if `to` and `from` are the same address. - /// MUST emit a Transfer event with each successful call. - /// SHOULD throw if `amount` is zero. - /// MAY allow minting from zero address, burning to the zero address, - /// transferring between accounts, and transferring between contracts. - /// MAY limit interaction with non-participating `from` addresses. - /// @param _from Address from which to transfer EXP tokens. - /// @param _to Address to which EXP tokens at `from` address will transfer. - /// @param _amount Total EXP tokens to reallocate. - function transfer(address _from, address _to, uint256 _amount) external; - - /// @notice Return total EXP managed by this contract. - /// @dev MUST sum EXP tokens of all `participant` addresses, - /// regardless of participation status, excluding only the zero address. - function totalSupply() external view returns (uint256); - - /// @notice Return total EXP allocated to a participant. - /// @dev MUST register each time `Transfer` emits. + /// @notice Remove a rating from an address. + /// MUST emit a Remove event with each successful call. + /// @param _removed Address to be removed. + function removeRating(address _removed) external; + + /// @notice Return a rated address' rating. + /// @dev MUST register each time `Rating` emits. /// SHOULD throw for queries about the zero address. - /// @param _participant An address for whom to query EXP total. - /// @return uint256 The number of EXP allocated to `participant`, possibly zero. - function balanceOf(address _participant) external view returns (uint256); + /// @param _rated An address for whom to query rating. + /// @return int8 The rating assigned. + function ratingOf(address _rated) external view returns (int8); } interface IERC165 { /// @notice Query if a contract implements an interface. - /// @dev Interface identification is specified in ERC-165. This function + /// @dev Interface identification is specified in EIP-165. This function /// uses less than 30,000 gas. - /// @param interfaceID The interface identifier, as specified in ERC-165. + /// @param interfaceID The interface identifier, as specified in EIP-165. /// @return bool `true` if the contract implements `interfaceID` and /// `interfaceID` is not 0xffffffff, `false` otherwise. function supportsInterface(bytes4 interfaceID) external view returns (bool); } ``` -The *metadata extension* is OPTIONAL for ERC-4974 smart contracts. This allows an EXP smart contract to be interrogated for its name and description. -``` -// SPDX-License-Identifier: CC0 +## Rationale -pragma solidity ^0.8.0; +### Rating Assignment -import "./IERC4974.sol"; +Ratings SHALL be at the sole discretion of the contract operator. This party may be a sports team coach or a multisig DAO wallet. We decide not to specify how governance occurs, but only *that* governance occurs. This allows for a wider range of potential use cases than optimizing for particular decision-making forms. -/// @title ERC-4974 EXP Token Standard, optional metadata extension -/// @dev See https://eips.ethereum.org/EIPS/EIP-4974 -/// Note: the ERC-165 identifier for this interface is 0x74793a15. -interface IERC4974Metadata is IERC4974 { - /// @notice A descriptive name for the EXP in this contract. - function name() external view returns (string memory); +This proposal standardizes a control mechanism to allocate community reputation without encouraging financialization of that recognition. While it does not ensure meritocracy, it opens the door. - /// @notice A one-line description of the EXP in this contract. - function description() external view returns (string memory); -} -``` +### Choice of int8 -## Rationale -### Participation -EXP drops SHALL require pre-approval from the delivery address. This ensures the receiver is a consenting participant in the smart contract. +It's signed: Reviewers should be able to give neutral and negative ratings for the wallets and contracts they interact with. This is especially important for decentralized applications that may be subject to malicious actors. -### Transfers -EXP transfers SHALL be at the sole discretion of the contract operator. This party may be a sports team coach or a multisig DAO wallet. We decide not to specify how governance occurs, but only *that* governance occurs. This allows for a wider range of potential use cases than optimizing for particular decision-making forms. +It's 8bit: The objective here is to keep ratings within some fathomably comparable range. Longer term, this could encourage easy aggregation of ratings, versus using larger numbers where users might employ a great variety of scales. -ERC-4974 standardizes a control mechanism to allocate community recognition without encouraging financialization of that recognition or easily allowing non-contributors to acquire EXP representing contribution. While it does not ensure meritocracy, it opens the door. +### Rating Changes -### Token Destruction -EXP SHOULD allow burning tokens by contract operators. If Bob has contributed greatly to the community, but then is caught stealing from Alice, the community may decide this should lower Bob's standing and influence in the community. Again, while this does not ensure an ethical standard within the community, it opens the door. +Ratings SHOULD allow rating updates by contract operators. If Bob has contributed greatly to the community, but then is caught stealing from Alice, the community may decide this should lower Bob's standing and influence in the community. Again, while this does not ensure an ethical standard within the community, it opens the door. -### EXP Word Choice -EXP, or experience points, are common parlance in the video game industry and generally known among modern internet users. Allocated EXP typically confers to strength and accumulates as one progresses in a game. This serves as a fair analogy to what we aim to achieve with ERC-4974 by encouraging members of a community to have more strength in that community the more they contribute. +Relatedly, ratings SHOULD allow removal of ratings to rescind a rating if the rater does not have confidence in their ability to rate effectively. -*Alternatives Considered: Soulbound Tokens, Soulbounds, Fungible Soulbound Tokens, Non-tradable Fungible Tokens, Non-transferrable Fungible Tokens, Karma Points, Reputation Tokens, Kudos* +### Interface Detection -### Participants Word Choice -Participants have agency over their *participation* in an activity, but not over the *outcomes*. Parties to ERC-4974 contracts are not owners in the same sense as owners of ERC-20 or ERC-721 tokens. The EXP sits in their wallets, but those wallets do not directly control any use of the EXP. +We chose Standard Interface Detection ([EIP-165](./eip-165.md)) to expose the interfaces that a compliant smart contract supports. -*Alternatives Considered: members, parties, contributors, players, entrants* +### Metadata Choices -### ERC-165 Interface -We chose Standard Interface Detection (ERC-165) to expose the interfaces that an ERC-4974 smart contract supports. +We have required `name` and `description` functions in the metadata extension. `name` common among major standards for blockchain-based primitives. We included a `description` function that may be helpful for games or other applications with multiple ratings systems. -### Metadata Choices -We have required `name` and `description` functions in the metadata extension. Name common among major token standards (namely, ERC-20 and ERC-721). We eschewed `symbol` as we do not wish them to be listed on any tickers that might tempt operators to engage in financial activities with these assets. We included a `description` function that may be helpful for games or other applications with multiple ERC-4974 tokens. +We remind implementation authors that the empty string is a valid response to `name` and `description` if you protest to the usage of this mechanism. We also remind everyone that any smart contract can use the same name and description as your contract. How a client may determine which ratings smart contracts are well-known (canonical) is outside the scope of this standard. + +### Drawbacks -We remind implementation authors that the empty string is a valid response to `name` and `description` if you protest to the usage of this mechanism. We also remind everyone that any smart contract can use the same name and symbol as your contract. How a client may determine which ERC-4974 smart contracts are well-known (canonical) is outside the scope of this standard. +One potential drawback of using this standard is that ratings are subjective and may not always accurately reflect the true value or quality of a contract or wallet. However, the standard provides mechanisms for updating and removing ratings, allowing for flexibility and evolution over time. -### Privacy -Users identified in the motivation section have a strong need to identify how much EXP an address holds. Since EXP contracts are opt-in, we hope users will be proud of their accumulated recognition and not wish to keep it secret. Without metadata associated to individual tokens or wallets, the privacy risks of this standard are limited. +Users identified in the motivation section have a strong need to identify how a contract or community evaluates another. While some users may be proud of ratings they receive, others may rightly or wrongly receive negative ratings from certain contracts. Negative ratings may allow for nefarious activities such as bullying and discrimination. We implore all implementers to be mindful of the consequences of any ratings systems they create with this standard. ## Backwards Compatibility -We have adopted `Transfer`, `transfer`, `balanceOf`, `totalSupply`, and `name` semantics from the ERC-20 and ERC-721 specifications. An implementation may also include a function `decimals` that returns `uint8(0)` if its goal is to be more compatible with ERC-20 while supporting this standard. + +We have adopted the `name` semantics from the EIP-20 and EIP-721 specifications. ## Reference Implementation + A reference implementation of this standard can be found in the assets folder. ## Security Considerations -The `operator` address has total control over the allocation and transfer of tokens. Therefore, ensuring this party is secure and trustworthy is critical for the contract to function. No alternative exists if the operator is corrupted or lost. -We strongly encourage `operator` to be a smart contract with robust access control features to manage EXP. +One potential security concern with this standard is the risk of malicious actors assigning false or misleading ratings to contracts or wallets. This could be used to manipulate voting weights in a DAO, or to deceive users into making poor decisions based on inaccurate ratings. + +To address this concern, the standard includes mechanisms for updating and removing ratings, allowing for corrections to be made in cases of false or misleading ratings. Additionally, the use of a single operator address to assign and update ratings provides a single point of control, which can be used to enforce rules and regulations around the assignment of ratings. + +Another potential security concern is the potential for an attacker to gain control of the operator address and use it to manipulate ratings for their own benefit. To mitigate this risk, it is recommended that the operator address be carefully managed and protected, and that multiple parties be involved in its control and oversight. + +Overall, the security of compliant contracts will depend on the careful management and protection of the operator address, as well as the development of clear rules and regulations around the assignment of ratings. ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + +Copyright and related rights waived via CC0. diff --git a/assets/eip-4974/ERC4974.sol b/assets/eip-4974/ERC4974.sol index 69a60601183157..2619a2e4ec7821 100644 --- a/assets/eip-4974/ERC4974.sol +++ b/assets/eip-4974/ERC4974.sol @@ -2,201 +2,83 @@ pragma solidity ^0.8.0; import "./IERC4974.sol"; -import "./IERC4974Metadata.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; -import "@openzeppelin/contracts/utils/Context.sol"; /** * See {IERC4974} * Implements the ERC4974 Metadata extension. */ -contract ERC4974 is Context, IERC165, IERC4974, IERC4974Metadata { - mapping(address => uint256) private _balances; - mapping(address => bool) private _participants; - address private _operator; - uint256 private _totalSupply; - string private _name; - string private _description; - - /** - * @notice Sets the values for {name} and {symbol}. - * @dev Name and description are both immutable: they can only be set once during - * construction. Operator cannot be the zero address. - */ - constructor(string memory name_, string memory description_, address operator_) { - // constructor(address operator_) { - require(operator_ != address(0), "Operator cannot be the zero address."); - _name = name_; - _description = description_; - _operator = operator_; - _participants[_operator] = true; - _participants[address(0)] = true; - } - - /** - * - * External Functions - * - */ +contract LoyaltyPoints is IERC4974 { - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165) returns (bool) { - return - interfaceId == type(IERC4974).interfaceId || - interfaceId == type(IERC4974Metadata).interfaceId || - supportsInterface(interfaceId); - } + // The address of the operator that can assign ratings + address private _operator; - /** - * @notice Assigns a new operator address. - * @dev Throws if sender is not operator or `newOperator` equals current `_operator` - * @param newOperator Address to reassign operator role. - */ - function setOperator(address newOperator) external virtual override { - _setOperator(newOperator); - } + // Mapping of customer addresses to their ratings + mapping (bytes32 => int8) private _ratings; - /** - * @notice Sets participation status for an address. - * @dev Throws if msg.sender is not the address in question. - */ - function setParticipation(address participant, bool participation) external virtual override { - _participation(participant, participation); + // Initializes the contract by setting the operator to msg.sender + constructor () { + _operator = msg.sender; } - /** - * @notice Transfer `amount` EXP to an account. - * @dev Emits `Transfer` event if successful. - * Throws if: - * - Sender is not operator - * - `to` address is not participating. - * - Zero address mints to itself. - * @param from The address from which to transfer. Zero address for mints. - * @param to The address of the recipient. Zero address for burns. - * @param amount The amount to be transferred. - */ - function transfer(address from, address to, uint256 amount) external virtual override { - _transfer(from, to, amount); + // Set the operator address + // Only the current operator or the contract owner can call this function + function setOperator(address newOperator) public override { + require(_operator == msg.sender || msg.sender == address(this), "Only the current operator or the contract owner can set the operator."); + _operator = newOperator; + emit NewOperator(_operator); } - /** - * @notice Returns the name of the EXP token. - * @return string The name of the EXP token. - */ - function name() external view virtual override returns (string memory) { - return _name; + // Rate a customer + // Only the operator can call this function + function rate(address customer, int8 rating) public override { + require(_operator == msg.sender, "Only the operator can assign ratings."); + bytes32 hash = keccak256(abi.encodePacked(customer)); + _ratings[hash] = rating; + emit Rating(customer, rating); } - /** - * @notice Returns the description of the EXP token, - * usually a one-line description. - * @return string The description of the EXP token. - */ - function description() external view virtual override returns (string memory) { - return _description; + // Remove a rating from a customer + // Only the operator can call this function + function removeRating(address customer) external override { + require(_operator == msg.sender, "Only the operator can remove ratings."); + bytes32 hash = keccak256(abi.encodePacked(customer)); + delete _ratings[hash]; + emit Removal(customer); } - /** - * @notice Returns the current operator of the EXP token, - * @return address The current operator of the EXP token. - */ - function operator() external view virtual returns (address) { + // Get the rating for a customer + function getOperator() public view returns (address) { return _operator; } - /** - * @notice Returns the EXP balance of the account. - * @param account The address to query. - * @return uint256 The EXP balance of the account. - */ - function balanceOf(address account) external view virtual override returns (uint256) { - require(account != address(0), "Zero address has no balance."); - return _balances[account]; - } + // Check if a customer has been rated + function hasBeenRated(address customer) public view returns (bool) { + // Hash the customer address + bytes32 hash = keccak256(abi.encodePacked(customer)); - /** - * @notice Returns the participation status of the account. - * @param account The address to query. - * @return bool The participation status of the queried account. - */ - function participationOf(address account) external view virtual returns (bool) { - return _participants[account]; + // Check if the hash exists in the mapping + return _ratings[hash] != 0; } - /** - * @notice Returns the total supply of EXP tokens. - * @dev Result includes inactive accounts, but not destroyed tokens. - * @return uint256 The total supply of EXP tokens. - */ - function totalSupply() external view virtual override returns (uint256) { - return _totalSupply; + function ratingOf(address _rated) public view override returns (int8) { + bytes32 hash = keccak256(abi.encodePacked(_rated)); + // Check if the customer has been rated + require(hasBeenRated(_rated), "This customer has not been rated yet."); + // Return the customer's rating + return _ratings[hash]; } - /** - * - * Internal Functions - * - */ - - /** - * @notice Assign a new operator. - * @dev Throws is sender is not current operator. - * Emits {Appointment} event. - * @param newOperator address to be assigned operator authority. - */ - function _setOperator(address newOperator) internal virtual { - require(_msgSender() == _operator, "Sender is not operator."); - require(newOperator != address(0), "Operator cannot be the zero address."); - require(_operator != newOperator, "{address} is already assigned as operator"); - _operator = newOperator; - emit Appointment(newOperator); + // Award ETH to a customer based on their rating + function awardEth(address payable customer) public payable { + // Calculate the amount of ETH to award based on the customer's rating + int8 rating = ratingOf(customer); + require(rating > 0, "Sorry, this customer has a rating less than 0 and cannot be awarded."); + uint256 award = uint256(int256(rating)); + // Transfer the ETH to the customer + require(address(this).balance >= award, "Contract has insufficient balance to award ETH."); + customer.transfer(award); } - /** - * @notice Sets participation status for `participant`. - * @dev Throws if sender is not `participant`. - * Emits a {Participation} event. - * @param participant Address for which to set participation. - * @param participation Requested participation status. - */ - function _participation(address participant, bool participation) internal virtual { - require(_msgSender() == participant, "Sender is not {participant}."); - require(_msgSender() != _operator, "Operator cannot change participation"); - require(participant != address(0), "Zero address cannot be removed."); - require(_participants[participant] != participation, "Participant already has {participation} status"); - _participants[participant] = participation; - emit Participation(participant, participation); - } + receive () external payable {} - /** - * @notice Moves `amount` of tokens from `from` address to `to` address. - * @dev Throws if sender is not operator. - * Throws if `to` is not participating. - * Emits a {Transfer} event. - * @param from Address from which to transfer. If zero address, then add to totalSupply. - * @param to Address to which to transfer. If zero address, then subtract from totalSupply. - * @param amount Number of EXP transfer. - */ - function _transfer( - address from, - address to, - uint256 amount - ) internal virtual { - require(_msgSender() == _operator, "Sender is not the operator."); - require(_participants[to] == true, "{to} address is not an active participant."); - require(from != to, "{to} and {from} cannot be the same address."); - if (from == address(0)) { - _totalSupply += amount; - } else { - require(_balances[from] >= amount, "{from} address holds less EXP than {amount}."); - _balances[from] -= amount; - if (to == address(0)) { - _totalSupply -= amount; - } - } - _balances[to] += amount; - emit Transfer(from, to, amount); - } } \ No newline at end of file diff --git a/assets/eip-4974/IERC4974.sol b/assets/eip-4974/IERC4974.sol index deda31190d9336..8043c6e0f5b6d6 100644 --- a/assets/eip-4974/IERC4974.sol +++ b/assets/eip-4974/IERC4974.sol @@ -2,27 +2,26 @@ pragma solidity ^0.8.0; -/// @title ERC-4974 Experience (EXP) Token Standard +/// @title EIP-4974 Ratings /// @dev See https://eips.ethereum.org/EIPS/EIP-4974 -/// Note: the ERC-165 identifier for this interface is 0x696e7752. +/// Note: the EIP-165 identifier for this interface is #######. /// Must initialize contracts with an `operator` address that is not `address(0)`. -/// Must initialize contracts assigning participation as `true` for both `operator` and `address(0)`. interface IERC4974 /* is ERC165 */ { /// @dev Emits when operator changes. /// MUST emit when `operator` changes by any mechanism. /// MUST ONLY emit by `setOperator`. - event Appointment(address indexed _operator); + event NewOperator(address indexed _operator); - /// @dev Emits when an address activates or deactivates its participation. - /// MUST emit emit when participation status changes by any mechanism. - /// MUST ONLY emit by `setParticipation`. - event Participation(address indexed _participant, bool _participation); + /// @dev Emits when operator issues a rating. + /// MUST emit when rating is assigned by any mechanism. + /// MUST ONLY emit by `rate`. + event Rating(address _rated, int8 _rating); - /// @dev Emits when operator transfers EXP. - /// MUST emit when EXP is transferred by any mechanism. - /// MUST ONLY emit by `transfer`. - event Transfer(address indexed _from, address indexed _to, uint256 _amount); + /// @dev Emits when operator removes a rating. + /// MUST emit when rating is removed by any mechanism. + /// MUST ONLY emit by `remove`. + event Removal(address _removed); /// @notice Appoint operator authority. /// @dev MUST throw unless `msg.sender` is `operator`. @@ -32,38 +31,31 @@ interface IERC4974 /* is ERC165 */ { /// @param _operator New operator of the smart contract. function setOperator(address _operator) external; - /// @notice Activate or deactivate participation. CALLER IS RESPONSIBLE TO - /// UNDERSTAND THE TERMS OF THEIR PARTICIPATION. - /// @dev MUST throw unless `msg.sender` is `participant`. - /// MUST throw if `participant` is `operator` or zero address. - /// MUST emit a `Participation` event for status changes. - /// @param _participant Address opting in or out of participation. - /// @param _participation Participation status of _participant. - function setParticipation(address _participant, bool _participation) external; + /// @notice Rate an address. + /// MUST emit a Rating event with each successful call. + /// @param _rated Address to be rated. + /// @param _rating Total EXP tokens to reallocate. + function rate(address _rated, int8 _rating) external; - /// @notice Transfer EXP from one address to a participating address. - /// @dev MUST throw unless `msg.sender` is `operator`. - /// MUST throw unless `to` address is participating. - /// MUST throw if `to` and `from` are the same address. - /// MUST emit a Transfer event with each successful call. - /// SHOULD throw if `amount` is zero. - /// MAY allow minting from zero address, burning to the zero address, - /// transferring between accounts, and transferring between contracts. - /// MAY limit interaction with non-participating `from` addresses. - /// @param _from Address from which to transfer EXP tokens. - /// @param _to Address to which EXP tokens at `from` address will transfer. - /// @param _amount Total EXP tokens to reallocate. - function transfer(address _from, address _to, uint256 _amount) external; - - /// @notice Return total EXP managed by this contract. - /// @dev MUST sum EXP tokens of all `participant` addresses, - /// regardless of participation status, excluding only the zero address. - function totalSupply() external view returns (uint256); + /// @notice Remove a rating from an address. + /// MUST emit a Remove event with each successful call. + /// @param _removed Address to be removed. + function removeRating(address _removed) external; - /// @notice Return total EXP allocated to a participant. - /// @dev MUST register each time `Transfer` emits. + /// @notice Return a rated address' rating. + /// @dev MUST register each time `Rating` emits. /// SHOULD throw for queries about the zero address. - /// @param _participant An address for whom to query EXP total. - /// @return uint256 The number of EXP allocated to `participant`, possibly zero. - function balanceOf(address _participant) external view returns (uint256); + /// @param _rated An address for whom to query rating. + /// @return int8 The rating assigned. + function ratingOf(address _rated) external view returns (int8); +} + +interface IERC165 { + /// @notice Query if a contract implements an interface. + /// @dev Interface identification is specified in EIP-165. This function + /// uses less than 30,000 gas. + /// @param interfaceID The interface identifier, as specified in EIP-165. + /// @return bool `true` if the contract implements `interfaceID` and + /// `interfaceID` is not 0xffffffff, `false` otherwise. + function supportsInterface(bytes4 interfaceID) external view returns (bool); } \ No newline at end of file From 0c93c12b94eb26fb385368804892efeae5a67905 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 15 Dec 2022 01:07:07 -0800 Subject: [PATCH 041/274] Update ERC-5453 (#6140) * Update 5453 * Update ERC-5453 * Update ERC-5453 * Add assets * Fix updates * Fix updates * Fix updates * Fix updates --- EIPS/eip-5453.md | 344 ++++++++++++++---- assets/eip-5453/AERC5453.sol | 271 ++++++++++++++ assets/eip-5453/EndorsableERC721.sol | 47 +++ assets/eip-5453/IERC5453.sol | 65 ++++ .../eip-5453/ThresholdMultiSigForwarder.sol | 56 +++ 5 files changed, 712 insertions(+), 71 deletions(-) create mode 100644 assets/eip-5453/AERC5453.sol create mode 100644 assets/eip-5453/EndorsableERC721.sol create mode 100644 assets/eip-5453/IERC5453.sol create mode 100644 assets/eip-5453/ThresholdMultiSigForwarder.sol diff --git a/EIPS/eip-5453.md b/EIPS/eip-5453.md index f14e506fd7fc5d..96d6107f26e710 100644 --- a/EIPS/eip-5453.md +++ b/EIPS/eip-5453.md @@ -1,137 +1,339 @@ --- eip: 5453 -title: Smart Contract Crypto Endorsement -description: A data format for including digital signatures in a smart contract function call. +title: Endorsement - Permit for Any Functions +description: A general protocol for approving function calls in the same transaction rely on EIP-5750. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc-5453-endorsement-standard/10355 status: Draft type: Standards Track category: ERC created: 2022-08-12 -requires: 165 +requires: 165, 712, 1271, 5750 --- ## Abstract -Provides a format for endorsement of smart contract transactions in the format of extra data (the last bytes field of a method signature). +This EIP establish a general protocol for permitting approving function calls in the same transaction rely on [EIP-5750](./eip-5750.md). +Unlike a few prior art ([EIP-2612](./eip-2612.md) for [EIP-20](./eip-20.md), [EIP-4494](./eip-4494.md) for [EIP-721](./eip-721.md) that +usually only permit for a single behavior (`transfer` for EIP-20 and `safeTransferFrom` for EIP-721) and a single approver in two transactions (first a `permit(...)` TX, then a `transfer`-like TX), this EIP provides a way to permit arbitrary behaviors and aggregating multiple approvals from arbitrary number of approvers in the same transaction, allowing for Multi-Sig or Threshold Signing behavior. + ## Motivation -1. Support a second approval from another user. -2. Support pay-for-by another user -3. Support multi-sig -4. Support persons acting in concert by endorsements -5. Support accumulated voting -6. Support off-line signatures +1. Support permit(approval) alongside a function call. +2. Support a second approval from another user. +3. Support pay-for-by another user +4. Support multi-sig +5. Support persons acting in concert by endorsements +6. Support accumulated voting +7. Support off-line signatures + + ## 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 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. + +### Interfaces + +The interfaces and structure referenced here are as followed + + + +```solidity +pragma solidity ^0.8.9; + +struct ValidityBound { + bytes32 functionParamStructHash; + uint256 validSince; + uint256 validBy; + uint256 nonce; +} + +struct SingleEndorsementData { + address endorserAddress; // 32 + bytes sig; // dynamic = 65 +} + +struct GeneralExtensionDataStruct { + bytes32 erc5453MagicWord; + uint256 erc5453Type; + uint256 nonce; + uint256 validSince; + uint256 validBy; + bytes endorsementPayload; +} + +interface IERC5453EndorsementCore { + function eip5453Nonce(address endorser) external view returns (uint256); + function isEligibleEndorser(address endorser) external view returns (bool); +} + +interface IERC5453EndorsementDigest { + function computeValidityDigest( + bytes32 _functionParamStructHash, + uint256 _validSince, + uint256 _validBy, + uint256 _nonce + ) external view returns (bytes32); + + function computeFunctionParamHash( + string memory _functionName, + bytes memory _functionParamPacked + ) external view returns (bytes32); +} + +interface IERC5453EndorsementDataTypeA { + function computeExtensionDataTypeA( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address endorserAddress, + bytes calldata sig + ) external view returns (bytes memory); +} -1. For any complying method of compliant contract, the last data field MUST be `bytes`. -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`. +interface IERC5453EndorsementDataTypeB { + function computeExtensionDataTypeB( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address[] calldata endorserAddress, + bytes[] calldata sigs + ) external view returns (bytes memory); +} +``` -3. For `Erc5453FormatType == FIRST_TYPE_SINGLE_ENDORSER`, in which FIRST_TYPE_SINGLE_ENDORSER = `0x01`, it supports a simple endorsement signature in the following format. +### Behavior specification -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. +As specified in [EIP-5750 General Extensibility for Method Behaviors](./eip-5750.md), any compliant method that has an `bytes extraData` as its +last method designated for extending behaviors can conform to [EIP-5453](./eip-5453.md) as the way to indicate a permit from certain user. -5. The format of `FIRST_TYPE_SINGLE_ENDORSER` type is +1. Any compliant method of this EIP MUST be a [EIP-5750](./eip-5750.md) compliant method. +2. Caller MUST pass in the last parameter `bytes extraData` conforming a solidity memory encoded layout bytes of `GeneralExtensonDataStruct` specified in _Section Interfaces_. The following descriptions are based on when decoding `bytes extraData` into a `GeneralExtensonDataStruct` +3. In the `GeneralExtensonDataStruct`-decoded `extraData`, caller MUST set the value of `GeneralExtensonDataStruct.erc5453MagicWord` to be the `keccak256("ERC5453-ENDORSEMENT")`. +4. Caller MUST set the value of `GeneralExtensonDataStruct.erc5453Type` to be one of the supported values. -```text -endorser_addr || endorser_nonce || valid_by || r , s, v || Erc5453FormatType || MAGIC_WORLD +```solidity +uint256 constant ERC5453_TYPE_A = 1; +uint256 constant ERC5453_TYPE_B = 2; ``` -Whereas +5. When the value of `GeneralExtensonDataStruct.erc5453Type` is set to be `ERC5453_TYPE_A`, `GeneralExtensonDataStruct.endorsementPayload` MUST be abi encoded bytes of a `SingleEndorsementData`. +6. When the value of `GeneralExtensonDataStruct.erc5453Type` is set to be `ERC5453_TYPE_B`, `GeneralExtensonDataStruct.endorsementPayload` MUST be abi encoded bytes of `SingleEndorsementData[]` (a dynamic array). + +7. Each `SingleEndorsementData` MUST have a `address endorserAddress;` and a 65-bytes `bytes sig` signature. -- `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 +8. Each `bytes sig` MUST be an ECDSA (secp256k1) signature using private key of signer whose corresponding address is `endorserAddress` signing `validityDigest` which is the a hashTypeDataV4 of [EIP-712](./eip-712.md) of hashStruct of `ValidityBound` data structure as followed: -The total length denoted as `LENGTH_OF_ENDORSEMENT` for `FIRST_TYPE_SINGLE_ENDORSER` = 193 bytes` +```solidity +bytes32 validityDigest = + eip712HashTypedDataV4( + keccak256( + abi.encode( + keccak256( + "ValidityBound(bytes32 functionParamStructHash,uint256 validSince,uint256 validBy,uint256 nonce)" + ), + functionParamStructHash, + _validSince, + _validBy, + _nonce + ) + ) + ); +``` + -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. +9. The `functionParamStructHash` MUST be computed as followed ```solidity -event OnEndorsed(byte4 indexed methodSelector, address[] memory endorsers); + bytes32 functionParamStructHash = keccak256( + abi.encodePacked( + keccak256(bytes(_functionStructure)), + _functionParamPacked + ) + ); + return functionParamStructHash; ``` +whereas + +- `_functionStructure` MUST be computed as `function methodName(type1 param1, type2 param2, ...)`. +- `_functionParamPacked` MUST be computed as `enc(param1) || enco(param2) ...` + +10. Upon validating that `endorserAddress == ecrecover(validityDigest, signature)` or `EIP1271(endorserAddress).isValidSignature(validityDigest, signature) == ERC1271.MAGICVALUE`, the single endorsement MUST be deemed valid. +11. Compliant method MAY choose to impose a threshold for a number of endorsements needs to be valid in the same `ERC5453_TYPE_B` kind of `endorsementPayload`. + +12. The `validSince` and `validBy` are both inclusive. Implementer MAY choose to use blocknumber or timestamp. Implementor SHOULD find away to indicate whether `validSince` and `validBy` is blocknumber or timestamp. + ## 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: +1. We chose to have both `ERC5453_TYPE_A`(single-endorsement) and `ERC5453_TYPE_B`(multiple-endorsements, same nonce for entire contract) so we +could balance a wider range of use cases. E.g. the same use cases of EIP-2612 and EIP-4494 can be supported by `ERC5453_TYPE_A`. And threshold approvals can be done via `ERC5453_TYPE_B`. More complicated approval types can also be extended by defining new `ERC5453_TYPE_?` + +2. We chose to include both `validSince` and `validBy` to allow maximum flexibility in expiration. This can be also be supported by EVM natively at if adopted [EIP-5081](./eip-5081.md) but EIP-5081 will not be adopted anytime soon, we choose to add these two numbers in our protocol to allow +smart contract level support. -- 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. ## Reference Implementation +In addition to the specified algorithm for validating endorser signatures, we also present the following reference implementations. + ```solidity -// Macro is not supported yet in -// Implementer can just replicate the following expansion by hand until -// The macro is supported in solidity. -#define REMAIN_LENGTH extraData.length - LENGTH_OF_ENDORSEMENT; - -abstract contract ERC5453 { - modifier onlyEndorsed(bytes32 _msgDigest, bytes calldata _end/**orsement**/) { - require(_end.length == LENGTH_OF_ENDORSEMENT); - require(_end[_end.length - 8:] == MAGIC_WORLD); - - - // ERC-1271 prefix. See https://eips.ethereum.org/EIPS/eip-1271 - string memory erc1271Prefix = "\x19Ethereum Signed Message:\n32"; - bytes32 erc1271Hash = keccak256(abi.encodePacked(erc1271Prefix, _msgDigest)); - address endorser = ecrecover(erc1271Hash, - uint8(_end[128]), // v - bytes32(_end[64:96]), // r - bytes32(_eSig[96:128]) // s +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import "./IERC5453.sol"; + +abstract contract AERC5453Endorsible is EIP712, + IERC5453EndorsementCore, IERC5453EndorsementDigest, IERC5453EndorsementDataTypeA, IERC5453EndorsementDataTypeB { + // ... + + function _validate( + bytes32 msgDigest, + SingleEndorsementData memory endersement + ) internal virtual { + require( + endersement.sig.length == 65, + "AERC5453Endorsible: wrong signature length" + ); + require( + SignatureChecker.isValidSignatureNow( + endersement.endorserAddress, + msgDigest, + endersement.sig + ), + "AERC5453Endorsible: invalid signature" ); - // _isEligibleEndorser is application specific. - require (_isEligibleEndorser(endorser), "Endorser is not eligible to transfer."); + } + // ... + + modifier onlyEndorsed( + bytes32 _functionParamStructHash, + bytes calldata _extensionData + ) { + require(_isEndorsed(_functionParamStructHash, _extensionData)); _; } + + function computeExtensionDataTypeB( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address[] calldata endorserAddress, + bytes[] calldata sigs + ) external pure override returns (bytes memory) { + require(endorserAddress.length == sigs.length); + SingleEndorsementData[] + memory endorsements = new SingleEndorsementData[]( + endorserAddress.length + ); + for (uint256 i = 0; i < endorserAddress.length; ++i) { + endorsements[i] = SingleEndorsementData( + endorserAddress[i], + sigs[i] + ); + } + return + abi.encode( + GeneralExtensionDataStruct( + MAGIC_WORLD, + ERC5453_TYPE_B, + nonce, + validSince, + validBy, + abi.encode(endorsements) + ) + ); + } } -contract EndorsableERC721 is SomeERC721, ERC5453 { - function safeTransferFrom( - address from, - address to, - uint256 id, - bytes calldata extraData) +``` - onlyEndorsed( // used as modifier - keccak256( - abi.encodePacked( - from, to, id, amount, - extraData[:REMAIN_LENGTH], // first part of extraData is reserved for original use for extraData unendorsed. - extraData[REMAIN_LENGTH: REMAIN_LENGTH + 32], // nonce of endorsement for the {contract, endorser} combination - extraData[REMAIN_LENGTH + 32: REMAIN_LENGTH + 64], // valid_by for the endorsement +### Reference Implementation of `EndorsableERC721` + +Here is a reference implementation of `EndorsableERC721` that achieves similar behavior of [EIP-4494](./eip-4494.md). + +```solidity +pragma solidity ^0.8.9; + +contract EndorsableERC721 is ERC721, AERC5453Endorsible { + //... + + function mint( + address _to, + uint256 _tokenId, + bytes calldata _extraData + ) + external + onlyEndorsed( + _computeFunctionParamHash( + "function mint(address _to,uint256 _tokenId)", + abi.encode(_to, _tokenId) ), - data[REMAIN_LENGTH:] // the endorsement component - ) { - super.safeTransferFrom(from, to, id, extraData[:REMAIN_LENGTH]); // original - } + _extraData + ) + { + _mint(_to, _tokenId); + } +} +``` + +### Reference Implementation of `ThresholdMultiSigForwarder` + +Here is a reference implementation of ThresholdMultiSigForwarder that achieves similar behavior of multi-sig threshold approval +remote contract call like a Gnosis-Safe wallet. + +```solidity +pragma solidity ^0.8.9; + +contract ThresholdMultiSigForwarder is AERC5453Endorsible { + //... + function forward( + address _dest, + uint256 _value, + uint256 _gasLimit, + bytes calldata _calldata, + bytes calldata _extraData + ) + external + onlyEndorsed( + _computeFunctionParamHash( + "function forward(address _dest,uint256 _value,uint256 _gasLimit,bytes calldata _calldata)", + abi.encode(_dest, _value, _gasLimit, keccak256(_calldata)) + ), + _extraData + ) + { + string memory errorMessage = "Fail to call remote contract"; + (bool success, bytes memory returndata) = _dest.call{value: _value}( + _calldata + ); + Address.verifyCallResult(success, returndata, errorMessage); + } + } + ``` ## 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. +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`, `validSince`, and `validBy` 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 + Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5453/AERC5453.sol b/assets/eip-5453/AERC5453.sol new file mode 100644 index 00000000000000..f3b4d762a68669 --- /dev/null +++ b/assets/eip-5453/AERC5453.sol @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: CC0.0 OR Apache-2.0 +// Author: Zainan Victor Zhou +// See a full runnable hardhat project in https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5453 +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +import "./IERC5453.sol"; + +abstract contract AERC5453Endorsible is EIP712, + IERC5453EndorsementCore, IERC5453EndorsementDigest, IERC5453EndorsementDataTypeA, IERC5453EndorsementDataTypeB { + uint256 private threshold; + uint256 private currentNonce = 0; + bytes32 constant MAGIC_WORLD = keccak256("ERC5453-ENDORSEMENT"); // ASCII of "ENDORSED" + uint256 constant ERC5453_TYPE_A = 1; + uint256 constant ERC5453_TYPE_B = 2; + + constructor( + string memory _name, + string memory _erc721Version + ) EIP712(_name, _erc721Version) {} + + function _validate( + bytes32 msgDigest, + SingleEndorsementData memory endersement + ) internal virtual { + require( + endersement.sig.length == 65, + "AERC5453Endorsible: wrong signature length" + ); + require( + SignatureChecker.isValidSignatureNow( + endersement.endorserAddress, + msgDigest, + endersement.sig + ), + "AERC5453Endorsible: invalid signature" + ); + } + + function _extractEndorsers( + bytes32 digest, + GeneralExtensionDataStruct memory data + ) internal virtual returns (address[] memory endorsers) { + require( + data.erc5453MagicWord == MAGIC_WORLD, + "AERC5453Endorsible: MagicWord not matched" + ); + require( + data.validSince <= block.number, + "AERC5453Endorsible: Not valid yet" + ); // TODO consider per-Endorser validSince + require(data.validBy >= block.number, "AERC5453Endorsible: Expired"); // TODO consider per-Endorser validBy + require( + currentNonce == data.nonce, + "AERC5453Endorsible: Nonce not matched" + ); // TODO consider per-Endorser nonce or range of nonce + currentNonce += 1; + + if (data.erc5453Type == ERC5453_TYPE_A) { + SingleEndorsementData memory endersement = abi.decode( + data.endorsementPayload, + (SingleEndorsementData) + ); + endorsers = new address[](1); + endorsers[0] = endersement.endorserAddress; + _validate(digest, endersement); + } else if (data.erc5453Type == ERC5453_TYPE_B) { + SingleEndorsementData[] memory endorsements = abi.decode( + data.endorsementPayload, + (SingleEndorsementData[]) + ); + endorsers = new address[](endorsements.length); + for (uint256 i = 0; i < endorsements.length; ++i) { + endorsers[i] = endorsements[i].endorserAddress; + _validate(digest, endorsements[i]); + } + return endorsers; + } + } + + function _decodeExtensionData( + bytes memory extensionData + ) internal pure virtual returns (GeneralExtensionDataStruct memory) { + return abi.decode(extensionData, (GeneralExtensionDataStruct)); + } + + // Well, I know this is epensive. Let's improve it later. + function _noRepeat(address[] memory _owners) internal pure returns (bool) { + for (uint256 i = 0; i < _owners.length; i++) { + for (uint256 j = i + 1; j < _owners.length; j++) { + if (_owners[i] == _owners[j]) { + return false; + } + } + } + return true; + } + + function _isEndorsed( + bytes32 _functionParamStructHash, + bytes calldata _extraData + ) internal returns (bool) { + GeneralExtensionDataStruct memory _data = _decodeExtensionData( + _extraData + ); + bytes32 finalDigest = _computeValidityDigest( + _functionParamStructHash, + _data.validSince, + _data.validBy, + _data.nonce + ); + + address[] memory endorsers = _extractEndorsers(finalDigest, _data); + require( + endorsers.length >= threshold, + "AERC5453Endorsable: not enough endorsers" + ); + require(_noRepeat(endorsers)); + for (uint256 i = 0; i < endorsers.length; i++) { + require( + _isEligibleEndorser(endorsers[i]), + "AERC5453Endorsable: not eligible endorsers" + ); // everyone must be a legit endorser + } + return true; + } + + function _isEligibleEndorser( + address /*_endorser*/ + ) internal view virtual returns (bool); + + modifier onlyEndorsed( + bytes32 _functionParamStructHash, + bytes calldata _extensionData + ) { + require(_isEndorsed(_functionParamStructHash, _extensionData)); + _; + } + + function _computeValidityDigest( + bytes32 _functionParamStructHash, + uint256 _validSince, + uint256 _validBy, + uint256 _nonce + ) internal view returns (bytes32) { + return + super._hashTypedDataV4( + keccak256( + abi.encode( + keccak256( + "ValidityBound(bytes32 functionParamStructHash,uint256 validSince,uint256 validBy,uint256 nonce)" + ), + _functionParamStructHash, + _validSince, + _validBy, + _nonce + ) + ) + ); + } + + function _computeFunctionParamHash( + string memory _functionStructure, + bytes memory _functionParamPacked + ) internal pure returns (bytes32) { + bytes32 functionParamStructHash = keccak256( + abi.encodePacked( + keccak256(bytes(_functionStructure)), + _functionParamPacked + ) + ); + return functionParamStructHash; + } + + function _setThreshold(uint256 _threshold) internal virtual { + threshold = _threshold; + } + + function computeValidityDigest( + bytes32 _functionParamStructHash, + uint256 _validSince, + uint256 _validBy, + uint256 _nonce + ) external view override returns (bytes32) { + return + _computeValidityDigest( + _functionParamStructHash, + _validSince, + _validBy, + _nonce + ); + } + + function computeFunctionParamHash( + string memory _functionName, + bytes memory _functionParamPacked + ) external pure override returns (bytes32) { + return + _computeFunctionParamHash( + _functionName, + _functionParamPacked + ); + } + + function eip5453Nonce(address addr) external view override returns (uint256) { + require(address(this) == addr, "AERC5453Endorsable: not self"); + return currentNonce; + } + + function isEligibleEndorser(address _endorser) + external + view + override + returns (bool) + { + return _isEligibleEndorser(_endorser); + } + + function computeExtensionDataTypeA( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address endorserAddress, + bytes calldata sig + ) external pure override returns (bytes memory) { + return + abi.encode( + GeneralExtensionDataStruct( + MAGIC_WORLD, + ERC5453_TYPE_A, + nonce, + validSince, + validBy, + abi.encode(SingleEndorsementData(endorserAddress, sig)) + ) + ); + } + + function computeExtensionDataTypeB( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address[] calldata endorserAddress, + bytes[] calldata sigs + ) external pure override returns (bytes memory) { + require(endorserAddress.length == sigs.length); + SingleEndorsementData[] + memory endorsements = new SingleEndorsementData[]( + endorserAddress.length + ); + for (uint256 i = 0; i < endorserAddress.length; ++i) { + endorsements[i] = SingleEndorsementData( + endorserAddress[i], + sigs[i] + ); + } + return + abi.encode( + GeneralExtensionDataStruct( + MAGIC_WORLD, + ERC5453_TYPE_B, + nonce, + validSince, + validBy, + abi.encode(endorsements) + ) + ); + } +} diff --git a/assets/eip-5453/EndorsableERC721.sol b/assets/eip-5453/EndorsableERC721.sol new file mode 100644 index 00000000000000..3299ef8fac527d --- /dev/null +++ b/assets/eip-5453/EndorsableERC721.sol @@ -0,0 +1,47 @@ +/// SPDX-License-Identifier: CC0.0 OR Apache-2.0 +// Author: Zainan Victor Zhou +// See a full runnable hardhat project in https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5453 +pragma solidity ^0.8.9; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +import "./AERC5453.sol"; + +contract EndorsableERC721 is ERC721, AERC5453Endorsible { + mapping(address => bool) private owners; + + constructor() + ERC721("ERC721ForTesting", "ERC721ForTesting") + AERC5453Endorsible("EndorsableERC721", "v1") + { + owners[msg.sender] = true; + } + + function addOwner(address _owner) external { + require(owners[msg.sender], "EndorsableERC721: not owner"); + owners[_owner] = true; + } + + function mint( + address _to, + uint256 _tokenId, + bytes calldata _extraData + ) + external + onlyEndorsed( + _computeFunctionParamHash( + "function mint(address _to,uint256 _tokenId)", + abi.encode(_to, _tokenId) + ), + _extraData + ) + { + _mint(_to, _tokenId); + } + + function _isEligibleEndorser( + address _endorser + ) internal view override returns (bool) { + return owners[_endorser]; + } +} diff --git a/assets/eip-5453/IERC5453.sol b/assets/eip-5453/IERC5453.sol new file mode 100644 index 00000000000000..d51df706cb0783 --- /dev/null +++ b/assets/eip-5453/IERC5453.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: CC0.0 OR Apache-2.0 +// Author: Zainan Victor Zhou +// See a full runnable hardhat project in https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5453 +pragma solidity ^0.8.9; + +struct ValidityBound { + bytes32 functionParamStructHash; + uint256 validSince; + uint256 validBy; + uint256 nonce; +} + +struct SingleEndorsementData { + address endorserAddress; // 32 + bytes sig; // dynamic = 65 +} + +struct GeneralExtensionDataStruct { + bytes32 erc5453MagicWord; + uint256 erc5453Type; + uint256 nonce; + uint256 validSince; + uint256 validBy; + bytes endorsementPayload; +} + +interface IERC5453EndorsementCore { + function eip5453Nonce(address endorser) external view returns (uint256); + function isEligibleEndorser(address endorser) external view returns (bool); +} + +interface IERC5453EndorsementDigest { + function computeValidityDigest( + bytes32 _functionParamStructHash, + uint256 _validSince, + uint256 _validBy, + uint256 _nonce + ) external view returns (bytes32); + + function computeFunctionParamHash( + string memory _functionName, + bytes memory _functionParamPacked + ) external view returns (bytes32); +} + +interface IERC5453EndorsementDataTypeA { + function computeExtensionDataTypeA( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address endorserAddress, + bytes calldata sig + ) external view returns (bytes memory); +} + + +interface IERC5453EndorsementDataTypeB { + function computeExtensionDataTypeB( + uint256 nonce, + uint256 validSince, + uint256 validBy, + address[] calldata endorserAddress, + bytes[] calldata sigs + ) external view returns (bytes memory); +} diff --git a/assets/eip-5453/ThresholdMultiSigForwarder.sol b/assets/eip-5453/ThresholdMultiSigForwarder.sol new file mode 100644 index 00000000000000..c366e6dcd47d5e --- /dev/null +++ b/assets/eip-5453/ThresholdMultiSigForwarder.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: CC0.0 OR Apache-2.0 +// Author: Zainan Victor Zhou +// See a full runnable hardhat project in https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5453 +pragma solidity ^0.8.9; + +import "./AERC5453.sol"; + +contract ThresholdMultiSigForwarder is AERC5453Endorsible { + mapping(address => bool) private owners; + uint256 private ownerCount; + + constructor() AERC5453Endorsible("ThresholdMultiSigForwarder", "v1") {} + + function initialize( + address[] calldata _owners, + uint256 _threshold + ) external { + require(_threshold >= 1, "Threshold must be positive"); + require(_owners.length >= _threshold); + require(_noRepeat(_owners)); + _setThreshold(_threshold); + for (uint256 i = 0; i < _owners.length; i++) { + owners[_owners[i]] = true; + } + ownerCount = _owners.length; + } + + function forward( + address _dest, + uint256 _value, + uint256 _gasLimit, + bytes calldata _calldata, + bytes calldata _extraData + ) + external + onlyEndorsed( + _computeFunctionParamHash( + "function forward(address _dest,uint256 _value,uint256 _gasLimit,bytes calldata _calldata)", + abi.encode(_dest, _value, _gasLimit, keccak256(_calldata)) + ), + _extraData + ) + { + string memory errorMessage = "Fail to call remote contract"; + (bool success, bytes memory returndata) = _dest.call{value: _value}( + _calldata + ); + Address.verifyCallResult(success, returndata, errorMessage); + } + + function _isEligibleEndorser( + address _endorser + ) internal view override returns (bool) { + return owners[_endorser] == true; + } +} From bd12378062fb7a51bec8e1a68e0b9fa24ec5ca5e Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 15 Dec 2022 01:16:53 -0800 Subject: [PATCH 042/274] Fix updates (#6141) --- EIPS/eip-5453.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5453.md b/EIPS/eip-5453.md index 96d6107f26e710..322cc3b2ceced4 100644 --- a/EIPS/eip-5453.md +++ b/EIPS/eip-5453.md @@ -105,6 +105,8 @@ interface IERC5453EndorsementDataTypeB { } ``` +See [`IERC5453.sol`](../assets/eip-5453/IERC5453.sol). + ### Behavior specification As specified in [EIP-5750 General Extensibility for Method Behaviors](./eip-5750.md), any compliant method that has an `bytes extraData` as its @@ -175,8 +177,6 @@ could balance a wider range of use cases. E.g. the same use cases of EIP-2612 an 2. We chose to include both `validSince` and `validBy` to allow maximum flexibility in expiration. This can be also be supported by EVM natively at if adopted [EIP-5081](./eip-5081.md) but EIP-5081 will not be adopted anytime soon, we choose to add these two numbers in our protocol to allow smart contract level support. - - ## 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. @@ -258,6 +258,8 @@ abstract contract AERC5453Endorsible is EIP712, ``` +See [`AERC5453.sol`](../assets/eip-5453/AERC5453.sol) + ### Reference Implementation of `EndorsableERC721` Here is a reference implementation of `EndorsableERC721` that achieves similar behavior of [EIP-4494](./eip-4494.md). @@ -287,6 +289,8 @@ contract EndorsableERC721 is ERC721, AERC5453Endorsible { } ``` +See [`EndorsableERC721.sol`](../assets/eip-5453/EndorsableERC721.sol) + ### Reference Implementation of `ThresholdMultiSigForwarder` Here is a reference implementation of ThresholdMultiSigForwarder that achieves similar behavior of multi-sig threshold approval @@ -324,6 +328,8 @@ contract ThresholdMultiSigForwarder is AERC5453Endorsible { ``` +See [`ThresholdMultiSigForwarder.sol`](../assets/eip-5453/ThresholdMultiSigForwarder.sol) + ## Security Considerations ### Replay Attacks From 55b5fc2ce4a7a8d11ad0f85d55bf04d66ade1ffe Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Thu, 15 Dec 2022 10:22:21 +0100 Subject: [PATCH 043/274] Add EIP-6059: Parent-Governed Nestable Non-Fungible Tokens standard (#6059) * Propose Parent-Governed Nestable Non-Fungible Tokens standard RMRK team has developed a next step in NFT evolution where one NFT can own and manage other NFTs. * Assign EIP number EIP number 6059 was assigned as this is the number of the PR to add this proposal. This was doen before being assigned the EIP number by one of the editors based on the common practice of assigning EIP numbers based on the PR number at which the proposal was added. In case the number of the proposal should be different, the number can easiliy be changed. * Add discussion URI and minor text-fixes The discussion URI was added and some minor text fixes addressed. * Update examples references * Update EIPS/eip-6059.md Uses "account" instead of "EOA" Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Apply grammar suggestions from code review Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Steven Pineda Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-6059.md | 532 ++++++ assets/eip-6059/contracts/INestable.sol | 113 ++ assets/eip-6059/contracts/NestableToken.sol | 1468 +++++++++++++++++ .../eip-6059/contracts/mocks/ERC721Mock.sol | 16 + .../contracts/mocks/NestableTokenMock.sol | 36 + assets/eip-6059/hardhat.config.ts | 21 + assets/eip-6059/package.json | 41 + assets/eip-6059/test/nestable.ts | 1158 +++++++++++++ 8 files changed, 3385 insertions(+) create mode 100644 EIPS/eip-6059.md create mode 100644 assets/eip-6059/contracts/INestable.sol create mode 100644 assets/eip-6059/contracts/NestableToken.sol create mode 100644 assets/eip-6059/contracts/mocks/ERC721Mock.sol create mode 100644 assets/eip-6059/contracts/mocks/NestableTokenMock.sol create mode 100644 assets/eip-6059/hardhat.config.ts create mode 100644 assets/eip-6059/package.json create mode 100644 assets/eip-6059/test/nestable.ts diff --git a/EIPS/eip-6059.md b/EIPS/eip-6059.md new file mode 100644 index 00000000000000..fe98f0b0fd8a51 --- /dev/null +++ b/EIPS/eip-6059.md @@ -0,0 +1,532 @@ +--- +eip: 6059 +title: Parent-Governed Nestable Non-Fungible Tokens +description: An interface for Nestable Non-Fungible Tokens with emphasis on parent token's control over the relationship. +author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) +discussions-to: https://ethereum-magicians.org/t/eip-6059-parent-governed-nestable-non-fungible-tokens/11914 +status: Draft +type: Standards Track +category: ERC +created: 2022-11-15 +requires: 165, 721 +--- + +## Abstract + +The Parent-Governed Nestable NFT standard extends [EIP-721](./eip-721.md) by allowing for a new inter-NFT relationship and interaction. + +At its core, the idea behind the proposal is simple: the owner of an NFT does not have to be an Externally Owned Account (EOA) or a smart contract, it can also be an NFT. + +The process of nesting an NFT into another is functionally identical to sending it to another user. The process of sending a token out of another one involves issuing a transaction from the account owning the parent token. + +An NFT can be owned by a single other NFT, but can in turn have a number of NFTs that it owns. This proposal establishes the framework for the parent-child relationships of NFTs. A parent token is the one that owns another token. A child token is a token that is owned by another token. A token can be both a parent and child at the same time. Child tokens of a given token can be fully managed by the parent token's owner, but can be proposed by anyone. + +```mermaid +graph LR + Z(EOA owning parent NFT) --> A[Parent NFT] + A --> B[Child NFT] + A --> C[Child NFT] + A --> D[Child NFT] + C --> E[Child's child NFT] + C --> F[Child's child NFT] + C --> G[Child's child NFT] +``` + +The graph illustrates how a child token can also be a parent token, but both are still administered by the root parent token's owner. + +## Motivation + +With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having the ability for tokens to own other tokens allows for greater utility, usability and forward compatibility. + +In the four years since [EIP-721](./eip-721.md) was published, the need for additional functionality has resulted in countless extensions. This EIP improves upon EIP-721 in the following areas: + +- [Bundling](#bundling) +- [Collecting](#collecting) +- [Membership](#membership) +- [Delegation](#delegation) + +### Bundling + +One of the most frequent uses of [EIP-721](./eip-721.md) is to disseminate the multimedia content that is tied to the tokens. In the event that someone wants to offer a bundle of NFTs from various collections, there is currently no easy way of bundling all of these together and handle their sale as a single transaction. This proposal introduces a standardized way of doing so. Nesting all of the tokens into a simple bundle and selling that bundle would transfer the control of all of the tokens to the buyer in a single transaction. + +### Collecting + +A lot of NFT consumers collect them based on countless criteria. Some aim for utility of the tokens, some for the uniqueness, some for the visual appeal, etc. There is no standardized way to group the NFTs tied to a specific account. By nesting NFTs based on their owner's preference, this proposal introduces the ability to do it. The root parent token could represent a certain group of tokens and all of the children nested into it would belong to it. + +The rise of soulbound, non-transferable, tokens, introduces another need for this proposal. Having a token with multiple soulbound traits (child tokens), allows for numerous use cases. One concrete example of this can be drawn from supply trains use case. A shipping container, represented by an NFT with its own traits, could have multiple child tokens denoting each leg of its journey. + +### Membership + +A common utility attached to NFTs is a membership to a Decentralised Autonomous Organization (DAO) or to some other closed-access group. Some of these organizations and groups occasionally mint NFTs to the current holders of the membership NFTs. With the ability to nest mint a token into a token, such minting could be simplified, by simply minting the bonus NFT directly into the membership one. + +### Delegation + +One of the core features of DAOs is voting and there are various approaches to it. One such mechanic is using fungible voting tokens where members can delegate their votes by sending these tokens to another member. Using this proposal, delegated voting could be handled by nesting your voting NFT into the one you are delegating your votes to and transferring it when the member no longer wishes to delegate their votes. + +## 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 +/// @title EIP-6059 Parent-Governed Nestable Non-Fungible Tokens +/// @dev See https://eips.ethereum.org/EIPS/eip-6059 +/// @dev Note: the ERC-165 identifier for this interface is 0x60b766e5. + +pragma solidity ^0.8.16; + +interface INestable { + /** + * @notice The core struct of ownership. + * @dev The `DirectOwner` struct is used to store information of the next immediate owner, be it the parent token, + * an `ERC721Receiver` contract or an externally owned account. + * @dev If the token is not owned by an NFT, the `tokenId` MUST equal `0`. + * @param tokenId ID of the parent token + * @param ownerAddress Address of the owner of the token. If the owner is another token, then the address MUST be + * the one of the parent token's collection smart contract. If the owner is externally owned account, the address + * MUST be the address of this account + * @param isNft A boolean value signifying whether the token is owned by another token (`true`) or by an externally + * owned account (`false`) + */ + struct DirectOwner { + uint256 tokenId; + address ownerAddress; + bool isNft; + } + + /** + * @notice Used to notify listeners that the token is being transferred. + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. + * @param from Address of the previous immediate owner, which is a smart contract if the token was nested. + * @param to Address of the new immediate owner, which is a smart contract if the token is being nested. + * @param fromTokenId ID of the previous parent token. If the token was not nested before, the value MUST be `0` + * @param toTokenId ID of the new parent token. If the token is not being nested, the value MUST be `0` + * @param tokenId ID of the token being transferred + */ + event NestTransfer( + address indexed from, + address indexed to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 indexed tokenId + ); + + /** + * @notice Used to notify listeners that a new token has been added to a given token's pending children array. + * @dev Emitted when a child NFT is added to a token's pending array. + * @param tokenId ID of the token that received a new pending child token + * @param childIndex Index of the proposed child token in the parent token's pending children array + * @param childAddress Address of the proposed child token's collection smart contract + * @param childId ID of the child token in the child token's collection smart contract + */ + event ChildProposed( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + /** + * @notice Used to notify listeners that a new child token was accepted by the parent token. + * @dev Emitted when a parent token accepts a token from its pending array, migrating it to the active array. + * @param tokenId ID of the token that accepted a new child token + * @param childIndex Index of the newly accepted child token in the parent token's active children array + * @param childAddress Address of the child token's collection smart contract + * @param childId ID of the child token in the child token's collection smart contract + */ + event ChildAccepted( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + /** + * @notice Used to notify listeners that all pending child tokens of a given token have been rejected. + * @dev Emitted when a token removes all a child tokens from its pending array. + * @param tokenId ID of the token that rejected all of the pending children + */ + event AllChildrenRejected(uint256 indexed tokenId); + + /** + * @notice Used to notify listeners a child token has been transferred from parent token. + * @dev Emitted when a token transfers a child from itself, transferring ownership. + * @param tokenId ID of the token that transferred a child token + * @param childIndex Index of a child in the array from which it is being transferred + * @param childAddress Address of the child token's collection smart contract + * @param childId ID of the child token in the child token's collection smart contract + * @param fromPending A boolean value signifying whether the token was in the pending child tokens array (`true`) or + * in the active child tokens array (`false`) + */ + event ChildTransferred( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId, + bool fromPending + ); + + /** + * @notice The core child token struct, holding the information about the child tokens. + * @return tokenId ID of the child token in the child token's collection smart contract + * @return contractAddress Address of the child token's smart contract + */ + struct Child { + uint256 tokenId; + address contractAddress; + } + + /** + * @notice Used to retrieve the *root* owner of a given token. + * @dev The *root* owner of the token is the top-level owner in the hierarchy which is not an NFT. + * @dev If the token is owned by another NFT, it MUST recursively look up the parent's root owner. + * @param tokenId ID of the token for which the *root* owner has been retrieved + * @return owner The *root* owner of the token + */ + function ownerOf(uint256 tokenId) external view returns (address owner); + + /** + * @notice Used to retrieve the immediate owner of the given token. + * @dev If the immediate owner is another token, the address returned, MUST be the one of the parent token's + * collection smart contract. + * @param tokenId ID of the token for which the direct owner is being retrieved + * @return address Address of the given token's owner + * @return uint256 The ID of the parent token. MUST be `0` if the owner is not an NFT + * @return bool The boolean value signifying whether the owner is an NFT or not + */ + function directOwnerOf(uint256 tokenId) + external + view + returns ( + address, + uint256, + bool + ); + + /** + * @notice Used to burn a given token. + * @dev When a token is burned, all of its child tokens are recursively burned as well. + * @dev When specifying the maximum recursive burns, the execution MUST be reverted if there are more children to be + * burned. + * @dev Setting the `maxRecursiveBurn` value to 0 SHOULD only attempt to burn the specified token and MUST revert if + * there are any child tokens present. + * @param tokenId ID of the token to burn + * @param maxRecursiveBurns Maximum number of tokens to recursively burn + * @return uint256 Number of recursively burned children + */ + function burn(uint256 tokenId, uint256 maxRecursiveBurns) + external + returns (uint256); + + /** + * @notice Used to add a child token to a given parent token. + * @dev This adds the child token into the given parent token's pending child tokens array. + * @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream + * child tokens. + * @dev This method MUST NOT be called directly. It MUST only be called from an instance of `INestable` as part of a + `nestMint`, `nestTransfer` or `transferChild` to an NFT. + * @dev Requirements: + * + * - `directOwnerOf` on the child contract MUST resolve to the called contract. + * - the pending array of the parent contract MUST not be full. + * @param parentId ID of the parent token to receive the new child token + * @param childId ID of the new proposed child token + */ + function addChild(uint256 parentId, uint256 childId) external; + + /** + * @notice Used to accept a pending child token for a given parent token. + * @dev This moves the child token from parent token's pending child tokens array into the active child tokens + * array. + * @param parentId ID of the parent token for which the child token is being accepted + * @param childIndex Index of the child token to accept in the pending children array of a given token + * @param childAddress Address of the collection smart contract of the child token expected to be at the specified + * index + * @param childId ID of the child token expected to be located at the specified index + */ + function acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) external; + + /** + * @notice Used to reject all pending children of a given parent token. + * @dev Removes the children from the pending array mapping. + * @dev The children's ownership structures are not updated. + * @dev Requirements: + * + * - `parentId` MUST exist + * @param parentId ID of the parent token for which to reject all of the pending tokens + * @param maxRejections Maximum number of expected children to reject, used to prevent from + * rejecting children which arrive just before this operation. + */ + function rejectAllChildren(uint256 parentId, uint256 maxRejections) external; + + /** + * @notice Used to transfer a child token from a given parent token. + * @dev MUST remove the child from the parent's active or pending children. + * @dev When transferring a child token, the owner of the token MUST be set to `to`, or not updated in the event of `to` + * being the `0x0` address. + * @param tokenId ID of the parent token from which the child token is being transferred + * @param to Address to which to transfer the token to + * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) + * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or + * pending array) + * @param childAddress Address of the child token's collection smart contract + * @param childId ID of the child token in its own collection smart contract + * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the + * parent token (`true`) or in the active array (`false`) + * @param data Additional data with no specified format, sent in call to `to` + */ + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes data + ) external; + + /** + * @notice Used to retrieve the active child tokens of a given parent token. + * @dev Returns array of Child structs existing for parent token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the active child tokens + * @return struct[] An array of Child structs containing the parent token's active child tokens + */ + function childrenOf(uint256 parentId) + external + view + returns (Child[] memory); + + /** + * @notice Used to retrieve the pending child tokens of a given parent token. + * @dev Returns array of pending Child structs existing for given parent. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the pending child tokens + * @return struct[] An array of Child structs containing the parent token's pending child tokens + */ + function pendingChildrenOf(uint256 parentId) + external + view + returns (Child[] memory); + + /** + * @notice Used to retrieve a specific active child token for a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the child is being retrieved + * @param index Index of the child token in the parent token's active child tokens array + * @return struct A Child struct containing data about the specified child + */ + function childOf(uint256 parentId, uint256 index) + external + view + returns (Child memory); + + /** + * @notice Used to retrieve a specific pending child token from a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the pending child token is being retrieved + * @param index Index of the child token in the parent token's pending child tokens array + * @return struct A Child struct containing data about the specified child + */ + function pendingChildOf(uint256 parentId, uint256 index) + external + view + returns (Child memory); + + /** + * @notice Used to transfer the token into another token. + * @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream + * child tokens. + * @param from Address of the direct owner of the token to be transferred + * @param to Address of the receiving token's collection smart contract + * @param tokenId ID of the token being transferred + * @param destinationId ID of the token to receive the token being transferred + */ + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId + ) external; +} +``` + +ID MUST never be a `0` value, as this proposal uses `0` values do signify that the token/destination is not an NFT. + +## Rationale + +Designing the proposal, we considered the following questions: + +1. **How to name the proposal?** + +In an effort to provide as much information about the proposal we identified the most important aspect of the proposal; the parent centered control over nesting. The child token's role is only to be able to be `Nestable` and support a token owning it. This is how we landed on the `Parent-Centered` part of the title. + +2. **Why is automatically accepting a child using [EIP-712](./eip-712.md) permit-style signatures not a part of this proposal?** + +For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with assets. + +3. **Why use indexes?** + +To reduce the gas consumption. If the token ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. Lists of active and pending children per token need to be maintained, since methods to get them are part of the proposed interface. + +To avoid race conditions in which the index of a token changes, the expected token ID as well as the expected token's collection smart contract is included in operations requiring token index, to verify that the token being accessed using the index is the expected one. + +Implementation that would internally keep track of indices using mapping was attempted. The minimum cost of accepting a child token was increased by over 20% and the cost of minting has increased by over 15%. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept the increased transaction cost this incurs. In the sample implementation provided, there are several hooks which make this possible. + +4. **Why is the pending children array limited instead of supporting pagination?** + +The pending child tokens array is not meant to be a buffer to collect the tokens that the root owner of the parent token wants to keep, but not enough to promote them to active children. It is meant to be an easily traversable list of child token candidates and should be regularly maintained; by either accepting or rejecting proposed child tokens. There is also no need for the pending child tokens array to be unbounded, because active child tokens array is. + +Another benefit of having bounded child tokens array is to guard against spam and griefing. As minting malicious or spam tokens could be relatively easy and low-cost, the bounded pending array assures that all of the tokens in it are easy to identify and that legitimate tokens are not lost in a flood of spam tokens, if one occurs. + +A consideration tied to this issue was also how to make sure, that a legitimate token is not accidentally rejected when clearing the pending child tokens array. We added the maximum pending children to reject argument to the clear pending child tokens array call. This assures that only the intended number of pending child tokens is rejected and if a new token is added to the pending child tokens array during the course of preparing such call and executing it, the clearing of this array SHOULD result in a reverted transaction. + +5. **Should we allow tokens to be nested into one of its children?** + +The proposal enforces that a parent token can't be nested into one of its child token, or downstream child tokens for that matter. A parent token and its children are all managed by the parent token's root owner. This means that if a token would be nested into one of its children, this would create the ownership loop and none of the tokens within the loop could be managed anymore. + +6. **Why is there not a "safe" nest transfer method?** + +`nestTransfer` is always "safe" since it MUST check for `INestable` compatibility on the destination. + +7. **How does this proposal differ from the other proposals trying to address a similar problem?** + +This interface allows for tokens to both be sent to and receive other tokens. The propose-accept and parent governed patterns allow for a more secure use. The backward compatibility is only added for EIP-721, allowing for a simpler interface. + +### Propose-Commit pattern for child token management + +Adding child tokens to a parent token MUST be done in the form of propose-commit pattern to allow for limited mutability by a 3rd party. When adding a child token to a parent token, it is first placed in a *"Pending"* array, and MUST be migrated to the *"Active"* array by the parent token's root owner. The *"Pending"* child tokens array SHOULD be limited to 128 slots to prevent spam and griefing. + +The limitation that only the root owner can accept the child tokens also introduces a trust inherent to the proposal. This ensures that the root owner of the token has full control over the token. No one can force the user to accept a child if they don't want to. + +### Parent Governed pattern + +The parent NFT of a nested token and the parent's root owner are in all aspects the true owners of it. Once you send a token to another one you give up ownership. + +We continue to use EIP-721's `ownerOf` functionality which will now recursively look up through parents until it finds an address which is not an NFT, this is referred to as the *root owner*. Additionally we provide the `directOwnerOf` which returns the most immediate owner of a token using 3 values: the owner address, the tokenId which MUST be 0 if the direct owner is not an NFT, and a flag indicating whether or not the parent is an NFT. + +The root owner or an approved party MUST be able do the following operations on children: `acceptChild`, `rejectAllChildren` and `transferChild`. + +The root owner or an approved party MUST also be allowed to do these operations only when token is not owned by an NFT: `transferFrom`, `safeTransferFrom`, `nestTransferFrom`, `burn`. + +If the token is owned by an NFT, only the parent NFT itself MUST be allowed to execute the operations listed above. Transfers MUST be done from the parent token, using `transferChild`, this method in turn SHOULD call `nestTransferFrom` or `safeTransferFrom` in the child token's smart contract, according to whether the destination is an NFT or not. For burning, tokens must first be transferred to an EOA and then burned. + +We add this restriction to prevent inconsistencies on parent contracts, since only the `transferChild` method takes care of removing the child from the parent when it is being transferred out of it. + +### Child token management + +This proposal introduces a number of child token management functions. In addition to the permissioned migration from *"Pending"* to *"Active"* child tokens array, the main token management function from this proposal is the `tranferChild` function. The following state transitions of a child token are available with it: + +1. Reject child token +2. Abandon child token +3. Unnest child token +4. Transfer the child token to an EOA or an `ERC721Receiver` +5. Transfer the child token into a new parent token + +To better understand how these state transitions are achieved, we have to look at the available parameters passed to `transferChild`: + +```solidity + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes data + ) external; +``` + +Based on the desired state transitions, the values of these parameters have to be set accordingly (any parameters not set in the following examples depend on the child token being managed): + +1. **Reject child token** + +```mermaid +graph LR + A(to = 0x0, isPending = true, destinationId = 0) -->|transferChild| B[Rejected child token] +``` + +2. **Abandon child token** + +```mermaid +graph LR + A(to = 0x0, isPending = false, destinationId = 0) -->|transferChild| B[Abandoned child token] +``` + +3. **Unnest child token** + +```mermaid +graph LR + A(to = rootOwner, destinationId = 0) -->|transferChild| B[Unnested child token] +``` + +4. **Transfer the child token to an EOA or an `ERC721Receiver`** + +```mermaid +graph LR + A(to = newEoAToReceiveTheToken, destinationId = 0) -->|transferChild| B[Transferred child token to EOA or ERC721Receiver] +``` + +5. **Transfer the child token into a new parent token** + +```mermaid +graph LR + A(to = collectionSmartContractOfNewParent, destinationId = IdOfNewParentToken) -->|transferChild| B[Transferred child token in a new parent token's pending array] +``` + +This state change places the token in the pending array of the new parent token. The child token still needs to be accepted by the new parent token's root owner in order to be placed into the active array of that token. + +## Backwards Compatibility + +The Nestable token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. + +## Test Cases + +Tests are included in [`nestable.ts`](../assets/eip-6059/test/nestable.ts). + +To run them in terminal, you can use the following commands: + +``` +cd ../assets/eip-6059 +npm install +npx hardhat test +``` + +## Reference Implementation + +See [`NestableToken.sol`](../assets/eip-6059/contracts/NestableToken.sol). + + +## Security Considerations + +The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more. + +Caution is advised when dealing with non-audited contracts. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-6059/contracts/INestable.sol b/assets/eip-6059/contracts/INestable.sol new file mode 100644 index 00000000000000..76e40ee107ab35 --- /dev/null +++ b/assets/eip-6059/contracts/INestable.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + + +interface INestable { + struct DirectOwner { + uint256 tokenId; + address ownerAddress; + bool isNft; + } + + event NestTransfer( + address indexed from, + address indexed to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 indexed tokenId + ); + + event ChildProposed( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + event ChildAccepted( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + event AllChildrenRejected(uint256 indexed tokenId); + + event ChildTransferred( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId, + bool fromPending + ); + + struct Child { + uint256 tokenId; + address contractAddress; + } + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function directOwnerOf( + uint256 tokenId + ) external view returns (address, uint256, bool); + + function burn( + uint256 tokenId, + uint256 maxRecursiveBurns + ) external returns (uint256); + + function addChild( + uint256 parentId, + uint256 childId, + bytes memory data + ) external; + + function acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) external; + + function rejectAllChildren(uint256 parentId, uint256 maxRejections) + external; + + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) external; + + function childrenOf( + uint256 parentId + ) external view returns (Child[] memory); + + function pendingChildrenOf( + uint256 parentId + ) external view returns (Child[] memory); + + function childOf( + uint256 parentId, + uint256 index + ) external view returns (Child memory); + + function pendingChildOf( + uint256 parentId, + uint256 index + ) external view returns (Child memory); + + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) external; +} diff --git a/assets/eip-6059/contracts/NestableToken.sol b/assets/eip-6059/contracts/NestableToken.sol new file mode 100644 index 00000000000000..840d44b9c8b58c --- /dev/null +++ b/assets/eip-6059/contracts/NestableToken.sol @@ -0,0 +1,1468 @@ +// SPDX-License-Identifier: CC0-1.0 + +//Generally all interactions should propagate downstream + +pragma solidity ^0.8.16; + +import "./INestable.sol"; +import "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.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/introspection/IERC165.sol"; + +error ChildAlreadyExists(); +error ChildIndexOutOfRange(); +error ERC721AddressZeroIsNotaValidOwner(); +error ERC721ApprovalToCurrentOwner(); +error ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); +error ERC721ApproveToCaller(); +error ERC721InvalidTokenId(); +error ERC721MintToTheZeroAddress(); +error ERC721NotApprovedOrOwner(); +error ERC721TokenAlreadyMinted(); +error ERC721TransferFromIncorrectOwner(); +error ERC721TransferToNonReceiverImplementer(); +error ERC721TransferToTheZeroAddress(); +error IdZeroForbidden(); +error IsNotContract(); +error MaxPendingChildrenReached(); +error MaxRecursiveBurnsReached(address childContract, uint256 childId); +error MintToNonNestableImplementer(); +error NestableTooDeep(); +error NestableTransferToDescendant(); +error NestableTransferToNonNestableImplementer(); +error NestableTransferToSelf(); +error NotApprovedOrDirectOwner(); +error PendingChildIndexOutOfRange(); +error UnexpectedChildId(); +error UnexpectedNumberOfChildren(); + +/** + * @title NestableToken + * @author RMRK team + * @notice Smart contract of the Nestable module. + * @dev This contract is hierarchy agnostic and can support an arbitrary number of nested levels up and down, as long as + * gas limits allow it. + */ +contract NestableToken is Context, IERC165, IERC721, INestable { + using Address for address; + + uint256 private constant _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP = 100; + + // Mapping owner address to token count + mapping(address => uint256) private _balances; + + // Mapping from token ID to approver address to approved address + // The approver is necessary so approvals are invalidated for nested children on transfer + // WARNING: If a child NFT returns to a previous root owner, old permissions would be active again + mapping(uint256 => mapping(address => address)) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // ------------------- NESTABLE -------------- + + // Mapping from token ID to DirectOwner struct + mapping(uint256 => DirectOwner) private _directOwners; + + // Mapping of tokenId to array of active children structs + mapping(uint256 => Child[]) private _activeChildren; + + // Mapping of tokenId to array of pending children structs + mapping(uint256 => Child[]) private _pendingChildren; + + // Mapping of child token address to child token ID to whether they are pending or active on any token + // We might have a first extra mapping from token ID, but since the same child cannot be nested into multiple tokens + // we can strip it for size/gas savings. + mapping(address => mapping(uint256 => uint256)) private _childIsInActive; + + // -------------------------- MODIFIERS ---------------------------- + + /** + * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. + * @dev If the caller is not the owner of the token or approved to manage it by its owner, the execution will be + * reverted. + * @param tokenId ID of the token to check + */ + function _onlyApprovedOrOwner(uint256 tokenId) private view { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) + revert ERC721NotApprovedOrOwner(); + } + + /** + * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. + * @param tokenId ID of the token to check + */ + modifier onlyApprovedOrOwner(uint256 tokenId) { + _onlyApprovedOrOwner(tokenId); + _; + } + + /** + * @notice Used to verify that the caller is approved to manage the given token or it its direct owner. + * @dev This does not delegate to ownerOf, which returns the root owner, but rater uses an owner from DirectOwner + * struct. + * @dev The execution is reverted if the caller is not immediate owner or approved to manage the given token. + * @dev Used for parent-scoped transfers. + * @param tokenId ID of the token to check. + */ + function _onlyApprovedOrDirectOwner(uint256 tokenId) private view { + if (!_isApprovedOrDirectOwner(_msgSender(), tokenId)) + revert NotApprovedOrDirectOwner(); + } + + /** + * @notice Used to verify that the caller is approved to manage the given token or is its direct owner. + * @param tokenId ID of the token to check + */ + modifier onlyApprovedOrDirectOwner(uint256 tokenId) { + _onlyApprovedOrDirectOwner(tokenId); + _; + } + + // ------------------------------- ERC721 --------------------------------- + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId || + interfaceId == type(INestable).interfaceId; + } + + /** + * @inheritdoc IERC721 + */ + function balanceOf(address owner) public view virtual returns (uint256) { + if (owner == address(0)) revert ERC721AddressZeroIsNotaValidOwner(); + return _balances[owner]; + } + + //////////////////////////////////////// + // TRANSFERS + //////////////////////////////////////// + + /** + * @inheritdoc IERC721 + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _transfer(from, to, tokenId); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _safeTransfer(from, to, tokenId, data); + } + + /** + * @notice Used to transfer the token into another token. + * @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream + * child tokens. + * @param from Address of the direct owner of the token to be transferred + * @param to Address of the receiving token's collection smart contract + * @param tokenId ID of the token being transferred + * @param destinationId ID of the token to receive the token being transferred + */ + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _nestTransfer(from, to, tokenId, destinationId, data); + } + + /** + * @notice Used to safely transfer the token form `from` to `to`. + * @dev The function checks that contract recipients are aware of the ERC721 protocol to prevent tokens from being + * forever locked. + * @dev This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. implement alternative + * mechanisms to perform token transfer, such as signature-based. + * @dev Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * @dev Emits a {Transfer} event. + * @param from Address of the account currently owning the given token + * @param to Address to transfer the token to + * @param tokenId ID of the token to transfer + * @param data Additional data with no specified format, sent in call to `to` + */ + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _transfer(from, to, tokenId); + if (!_checkOnERC721Received(from, to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @notice Used to transfer the token from `from` to `to`. + * @dev As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * @dev Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * @dev Emits a {Transfer} event. + * @param from Address of the account currently owning the given token + * @param to Address to transfer the token to + * @param tokenId ID of the token to transfer + */ + function _transfer( + address from, + address to, + uint256 tokenId + ) internal virtual { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(0)) revert ERC721TransferToTheZeroAddress(); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); + + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, 0, to, false); + _balances[to] += 1; + + emit Transfer(from, to, tokenId); + emit NestTransfer(immediateOwner, to, parentId, 0, tokenId); + + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); + } + + /** + * @notice Used to transfer a token into another token. + * @dev Attempting to nest a token into `0x0` address will result in reverted transaction. + * @dev Attempting to nest a token into itself will result in reverted transaction. + * @param from Address of the account currently owning the given token + * @param to Address of the receiving token's collection smart contract + * @param tokenId ID of the token to transfer + * @param destinationId ID of the token receiving the given token + * @param data Additional data with no specified format, sent in the addChild call + */ + function _nestTransfer( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) internal virtual { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(0)) revert ERC721TransferToTheZeroAddress(); + if (to == address(this) && tokenId == destinationId) + revert NestableTransferToSelf(); + + // Destination contract checks: + // It seems redundant, but otherwise it would revert with no error + if (!to.isContract()) revert IsNotContract(); + if (!IERC165(to).supportsInterface(type(INestable).interfaceId)) + revert NestableTransferToNonNestableImplementer(); + _checkForInheritanceLoop(tokenId, to, destinationId); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + to, + parentId, + destinationId, + tokenId + ); + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, destinationId, to, true); + _balances[to] += 1; + + // Sending to NFT: + _sendToNFT(immediateOwner, to, parentId, destinationId, tokenId, data); + } + + /** + * @notice Used to send a token to another token. + * @dev If the token being sent is currently owned by an externally owned account, the `parentId` should equal `0`. + * @dev Emits {Transfer} event. + * @dev Emits {NestTransfer} event. + * @param from Address from which the token is being sent + * @param to Address of the collection smart contract of the token to receive the given token + * @param parentId ID of the current parent token of the token being sent + * @param destinationId ID of the tokento receive the token being sent + * @param tokenId ID of the token being sent + * @param data Additional data with no specified format, sent in the addChild call + */ + function _sendToNFT( + address from, + address to, + uint256 parentId, + uint256 destinationId, + uint256 tokenId, + bytes memory data + ) private { + INestable destContract = INestable(to); + destContract.addChild(destinationId, tokenId, data); + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer(from, to, parentId, destinationId, tokenId); + + emit Transfer(from, to, tokenId); + emit NestTransfer(from, to, parentId, destinationId, tokenId); + } + + /** + * @notice Used to check if nesting a given token into a specified token would create an inheritance loop. + * @dev If a loop would occur, the tokens would be unmanageable, so the execution is reverted if one is detected. + * @dev The check for inheritance loop is bounded to guard against too much gas being consumed. + * @param currentId ID of the token that would be nested + * @param targetContract Address of the collection smart contract of the token into which the given token would be + * nested + * @param targetId ID of the token into which the given token would be nested + */ + function _checkForInheritanceLoop( + uint256 currentId, + address targetContract, + uint256 targetId + ) private view { + for (uint256 i; i < _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP; ) { + ( + address nextOwner, + uint256 nextOwnerTokenId, + bool isNft + ) = INestable(targetContract).directOwnerOf(targetId); + // If there's a final address, we're good. There's no loop. + if (!isNft) { + return; + } + // Ff the current nft is an ancestor at some point, there is an inheritance loop + if (nextOwner == address(this) && nextOwnerTokenId == currentId) { + revert NestableTransferToDescendant(); + } + // We reuse the parameters to save some contract size + targetContract = nextOwner; + targetId = nextOwnerTokenId; + unchecked { + ++i; + } + } + revert NestableTooDeep(); + } + + //////////////////////////////////////// + // MINTING + //////////////////////////////////////// + + /** + * @notice Used to safely mint a token to a specified address. + * @dev Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * @dev Emits a {Transfer} event. + * @param to Address to which to safely mint the gven token + * @param tokenId ID of the token to mint to the specified address + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @notice Used to safely mint the token to the specified address while passing the additional data to contract + * recipients. + * @param to Address to which to mint the token + * @param tokenId ID of the token to mint + * @param data Additional data to send with the tokens + */ + function _safeMint( + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _mint(to, tokenId); + if (!_checkOnERC721Received(address(0), to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @notice Used to mint a specified token to a given address. + * @dev WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible. + * @dev Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * @dev Emits a {Transfer} event. + * @param to Address to mint the token to + * @param tokenId ID of the token to mint + */ + function _mint(address to, uint256 tokenId) internal virtual { + _innerMint(to, tokenId, 0); + + emit Transfer(address(0), to, tokenId); + emit NestTransfer(address(0), to, 0, 0, tokenId); + + _afterTokenTransfer(address(0), to, tokenId); + _afterNestedTokenTransfer(address(0), to, 0, 0, tokenId); + } + + /** + * @notice Used to mint a child token to a given parent token. + * @param to Address of the collection smart contract of the token into which to mint the child token + * @param tokenId ID of the token to mint + * @param destinationId ID of the token into which to mint the new child token + * @param data Additional data with no specified format, sent in the addChild call + */ + function _nestMint( + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) internal virtual { + // It seems redundant, but otherwise it would revert with no error + if (!to.isContract()) revert IsNotContract(); + if (!IERC165(to).supportsInterface(type(INestable).interfaceId)) + revert MintToNonNestableImplementer(); + + _innerMint(to, tokenId, destinationId); + _sendToNFT(address(0), to, 0, destinationId, tokenId, data); + } + + /** + * @notice Used to mint a child token into a given parent token. + * @dev Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` must not exist. + * - `tokenId` must not be `0`. + * @param to Address of the collection smart contract of the token into which to mint the child token + * @param tokenId ID of the token to mint + * @param destinationId ID of the token into which to mint the new token + */ + function _innerMint( + address to, + uint256 tokenId, + uint256 destinationId + ) private { + if (to == address(0)) revert ERC721MintToTheZeroAddress(); + if (_exists(tokenId)) revert ERC721TokenAlreadyMinted(); + if (tokenId == 0) revert IdZeroForbidden(); + + _beforeTokenTransfer(address(0), to, tokenId); + _beforeNestedTokenTransfer(address(0), to, 0, destinationId, tokenId); + + _balances[to] += 1; + _directOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId, + isNft: destinationId != 0 + }); + } + + //////////////////////////////////////// + // Ownership + //////////////////////////////////////// + + /** + * @notice Used to retrieve the root owner of the given token. + * @dev Root owner is always the externally owned account. + * @dev If the given token is owned by another token, it will recursively query the parent tokens until reaching the + * root owner. + * @param tokenId ID of the token for which the root owner is being retrieved + * @return address Address of the root owner of the given token + */ + function ownerOf( + uint256 tokenId + ) public view virtual override(INestable, IERC721) returns (address) { + (address owner, uint256 ownerTokenId, bool isNft) = directOwnerOf( + tokenId + ); + if (isNft) { + owner = INestable(owner).ownerOf(ownerTokenId); + } + return owner; + } + + /** + * @notice Used to retrieve the immediate owner of the given token. + * @dev In the event the NFT is owned by an externally owned account, `tokenId` will be `0` and `isNft` will be + * `false`. + * @param tokenId ID of the token for which the immediate owner is being retrieved + * @return address Address of the immediate owner. If the token is owned by an externally owned account, its address + * will be returned. If the token is owned by another token, the parent token's collection smart contract address + * is returned + * @return uint256 Token ID of the immediate owner. If the immediate owner is an externally owned account, the value + * should be `0` + * @return bool A boolean value signifying whether the immediate owner is a token (`true`) or not (`false`) + */ + function directOwnerOf( + uint256 tokenId + ) public view virtual returns (address, uint256, bool) { + DirectOwner memory owner = _directOwners[tokenId]; + if (owner.ownerAddress == address(0)) revert ERC721InvalidTokenId(); + + return (owner.ownerAddress, owner.tokenId, owner.isNft); + } + + //////////////////////////////////////// + // BURNING + //////////////////////////////////////// + + /** + * @notice Used to burn a given token. + * @param tokenId ID of the token to burn + */ + function burn(uint256 tokenId) public virtual { + burn(tokenId, 0); + } + + /** + * @notice Used to burn a token. + * @dev When a token is burned, its children are recursively burned as well. + * @dev The approvals are cleared when the token is burned. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits a {Transfer} event. + * @param tokenId ID of the token to burn + * @param maxChildrenBurns Maximum children to recursively burn + * @return uint256 The number of recursive burns it took to burn all of the children + */ + function burn( + uint256 tokenId, + uint256 maxChildrenBurns + ) public virtual onlyApprovedOrDirectOwner(tokenId) returns (uint256) { + return _burn(tokenId, maxChildrenBurns); + } + + /** + * @notice Used to burn a token. + * @dev When a token is burned, its children are recursively burned as well. + * @dev The approvals are cleared when the token is burned. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits a {Transfer} event. + * @dev Emits a {NestTransfer} event. + * @param tokenId ID of the token to burn + * @param maxChildrenBurns Maximum children to recursively burn + * @return uint256 The number of recursive burns it took to burn all of the children + */ + function _burn( + uint256 tokenId, + uint256 maxChildrenBurns + ) internal virtual returns (uint256) { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + address owner = ownerOf(tokenId); + _balances[immediateOwner] -= 1; + + _beforeTokenTransfer(owner, address(0), tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId + ); + + _approve(address(0), tokenId); + _cleanApprovals(tokenId); + + Child[] memory children = childrenOf(tokenId); + + delete _activeChildren[tokenId]; + delete _pendingChildren[tokenId]; + delete _tokenApprovals[tokenId][owner]; + + uint256 pendingRecursiveBurns; + uint256 totalChildBurns; + + uint256 length = children.length; //gas savings + for (uint256 i; i < length; ) { + if (totalChildBurns >= maxChildrenBurns) + revert MaxRecursiveBurnsReached( + children[i].contractAddress, + children[i].tokenId + ); + delete _childIsInActive[children[i].contractAddress][ + children[i].tokenId + ]; + unchecked { + // At this point we know pendingRecursiveBurns must be at least 1 + pendingRecursiveBurns = maxChildrenBurns - totalChildBurns; + } + // We substract one to the next level to count for the token being burned, then add it again on returns + // This is to allow the behavior of 0 recursive burns meaning only the current token is deleted. + totalChildBurns += + INestable(children[i].contractAddress).burn( + children[i].tokenId, + pendingRecursiveBurns - 1 + ) + + 1; + unchecked { + ++i; + } + } + // Can't remove before burning child since child will call back to get root owner + delete _directOwners[tokenId]; + + _afterTokenTransfer(owner, address(0), tokenId); + _afterNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId + ); + emit Transfer(owner, address(0), tokenId); + emit NestTransfer(immediateOwner, address(0), parentId, 0, tokenId); + + return totalChildBurns; + } + + //////////////////////////////////////// + // APPROVALS + //////////////////////////////////////// + + /** + * @inheritdoc IERC721 + */ + function approve(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + if (to == owner) revert ERC721ApprovalToCurrentOwner(); + + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) + revert ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); + + _approve(to, tokenId); + } + + /** + * @inheritdoc IERC721 + */ + function getApproved( + uint256 tokenId + ) public view virtual returns (address) { + _requireMinted(tokenId); + + return _tokenApprovals[tokenId][ownerOf(tokenId)]; + } + + /** + * @inheritdoc IERC721 + */ + function setApprovalForAll(address operator, bool approved) public virtual { + if (_msgSender() == operator) revert ERC721ApproveToCaller(); + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @inheritdoc IERC721 + */ + function isApprovedForAll( + address owner, + address operator + ) public view virtual returns (bool) { + return _operatorApprovals[owner][operator]; + } + + /** + * @notice Used to grant an approval to manage a given token. + * @dev Emits an {Approval} event. + * @param to Address to which the approval is being granted + * @param tokenId ID of the token for which the approval is being granted + */ + function _approve(address to, uint256 tokenId) internal virtual { + address owner = ownerOf(tokenId); + _tokenApprovals[tokenId][owner] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @notice Used to update the owner of the token and clear the approvals associated with the previous owner. + * @dev The `destinationId` should equal `0` if the new owner is an externally owned account. + * @param tokenId ID of the token being updated + * @param destinationId ID of the token to receive the given token + * @param to Address of account to receive the token + * @param isNft A boolean value signifying whether the new owner is a token (`true`) or externally owned account + * (`false`) + */ + function _updateOwnerAndClearApprovals( + uint256 tokenId, + uint256 destinationId, + address to, + bool isNft + ) internal { + _directOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId, + isNft: isNft + }); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + _cleanApprovals(tokenId); + } + + /** + * @notice Used to remove approvals for the current owner of the given token. + * @param tokenId ID of the token to clear the approvals for + */ + function _cleanApprovals(uint256 tokenId) internal virtual {} + + //////////////////////////////////////// + // UTILS + //////////////////////////////////////// + + /** + * @notice Used to check whether the given account is allowed to manage the given token. + * @dev Requirements: + * + * - `tokenId` must exist. + * @param spender Address that is being checked for approval + * @param tokenId ID of the token being checked + * @return bool The boolean value indicating whether the `spender` is approved to manage the given token + */ + function _isApprovedOrOwner( + address spender, + uint256 tokenId + ) internal view virtual returns (bool) { + address owner = ownerOf(tokenId); + return (spender == owner || + isApprovedForAll(owner, spender) || + getApproved(tokenId) == spender); + } + + /** + * @notice Used to check whether the account is approved to manage the token or its direct owner. + * @param spender Address that is being checked for approval or direct ownership + * @param tokenId ID of the token being checked + * @return bool The boolean value indicating whether the `spender` is approved to manage the given token or its + * direct owner + */ + function _isApprovedOrDirectOwner( + address spender, + uint256 tokenId + ) internal view virtual returns (bool) { + (address owner, uint256 parentId, ) = directOwnerOf(tokenId); + // When the parent is an NFT, only it can do operations + if (parentId != 0) { + return (spender == owner); + } + // Otherwise, the owner or approved address can + return (spender == owner || + isApprovedForAll(owner, spender) || + getApproved(tokenId) == spender); + } + + /** + * @notice Used to enforce that the given token has been minted. + * @dev Reverts if the `tokenId` has not been minted yet. + * @dev The validation checks whether the owner of a given token is a `0x0` address and considers it not minted if + * it is. This means that both tokens that haven't been minted yet as well as the ones that have already been + * burned will cause the transaction to be reverted. + * @param tokenId ID of the token to check + */ + function _requireMinted(uint256 tokenId) internal view virtual { + if (!_exists(tokenId)) revert ERC721InvalidTokenId(); + } + + /** + * @notice Used to check whether the given token exists. + * @dev Tokens start existing when they are minted (`_mint`) and stop existing when they are burned (`_burn`). + * @param tokenId ID of the token being checked + * @return bool The boolean value signifying whether the token exists + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _directOwners[tokenId].ownerAddress != address(0); + } + + /** + * @notice Used to invoke {IERC721Receiver-onERC721Received} on a target address. + * @dev The call is not executed if the target address is not a contract. + * @param from Address representing the previous owner of the given token + * @param to Yarget address that will receive the tokens + * @param tokenId ID of the token to be transferred + * @param data Optional data to send along with the call + * @return bool Boolean value signifying whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory data + ) private returns (bool) { + if (to.isContract()) { + try + IERC721Receiver(to).onERC721Received( + _msgSender(), + from, + tokenId, + data + ) + returns (bytes4 retval) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721TransferToNonReceiverImplementer(); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + //////////////////////////////////////// + // CHILD MANAGEMENT PUBLIC + //////////////////////////////////////// + + /** + * @notice Used to add a child token to a given parent token. + * @dev This adds the iichild token into the given parent token's pending child tokens array. + * @dev You MUST NOT call this method directly. To add a a child to an NFT you must use either + * `nestTransfer`, `nestMint` or `transferChild` to the NFT. + * @dev Requirements: + * + * - `ownerOf` on the child contract must resolve to the called contract. + * - The pending array of the parent contract must not be full. + * @param parentId ID of the parent token to receive the new child token + * @param childId ID of the new proposed child token + * @param data Additional data with no specified format + */ + function addChild( + uint256 parentId, + uint256 childId, + bytes memory data + ) public virtual { + _requireMinted(parentId); + + address childAddress = _msgSender(); + if (!childAddress.isContract()) revert IsNotContract(); + + Child memory child = Child({ + contractAddress: childAddress, + tokenId: childId + }); + + _beforeAddChild(parentId, childAddress, childId); + + uint256 length = pendingChildrenOf(parentId).length; + + if (length < 128) { + _pendingChildren[parentId].push(child); + } else { + revert MaxPendingChildrenReached(); + } + + // Previous length matches the index for the new child + emit ChildProposed(parentId, length, childAddress, childId); + + _afterAddChild(parentId, childAddress, childId); + } + + /** + * @notice @notice Used to accept a pending child token for a given parent token. + * @dev This moves the child token from parent token's pending child tokens array into the active child tokens + * array. + * @param parentId ID of the parent token for which the child token is being accepted + * @param childIndex Index of a child tokem in the given parent's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) public virtual onlyApprovedOrOwner(parentId) { + _acceptChild(parentId, childIndex, childAddress, childId); + } + + /** + * @notice Used to accept a pending child token for a given parent token. + * @dev This moves the child token from parent token's pending child tokens array into the active child tokens + * array. + * @dev Requirements: + * + * - `tokenId` must exist + * - `index` must be in range of the pending children array + * @param parentId ID of the parent token for which the child token is being accepted + * @param childIndex Index of a child tokem in the given parent's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual { + if (pendingChildrenOf(parentId).length <= childIndex) + revert PendingChildIndexOutOfRange(); + + Child memory child = pendingChildOf(parentId, childIndex); + _checkExpectedChild(child, childAddress, childId); + if (_childIsInActive[childAddress][childId] != 0) + revert ChildAlreadyExists(); + + _beforeAcceptChild(parentId, childIndex, childAddress, childId); + + // Remove from pending: + _removeChildByIndex(_pendingChildren[parentId], childIndex); + + // Add to active: + _activeChildren[parentId].push(child); + _childIsInActive[childAddress][childId] = 1; // We use 1 as true + + emit ChildAccepted(parentId, childIndex, childAddress, childId); + + _afterAcceptChild(parentId, childIndex, childAddress, childId); + } + + /** + * @notice Used to reject all pending children of a given parent token. + * @dev Removes the children from the pending array mapping. + * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the + * rootOwner of the previous parent. + * @param tokenId ID of the parent token for which to reject all of the pending tokens + */ + function rejectAllChildren(uint256 tokenId, uint256 maxRejections) public virtual onlyApprovedOrOwner(tokenId) { + _rejectAllChildren(tokenId, maxRejections); + } + + /** + * @notice Used to reject all pending children of a given parent token. + * @dev Removes the children from the pending array mapping. + * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the + * rootOwner of the previous parent. + * @dev Requirements: + * + * - `tokenId` must exist + * @param tokenId ID of the parent token for which to reject all of the pending tokens. + * @param maxRejections Maximum number of expected children to reject, used to prevent from + * rejecting children which arrive just before this operation. + */ + function _rejectAllChildren(uint256 tokenId, uint256 maxRejections) + internal + virtual + { + if (_pendingChildren[tokenId].length > maxRejections) + revert UnexpectedNumberOfChildren(); + + _beforeRejectAllChildren(tokenId); + delete _pendingChildren[tokenId]; + emit AllChildrenRejected(tokenId); + _afterRejectAllChildren(tokenId); + } + + /** + * @notice Used to transfer a child token from a given parent token. + * @param tokenId ID of the parent token from which the child token is being transferred + * @param to Address to which to transfer the token to + * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) + * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or + * pending array) + * @param childAddress Address of the child token's collection smart contract. + * @param childId ID of the child token in its own collection smart contract. + * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the + * parent token (`true`) or in the active array (`false`) + * @param data Additional data with no specified format, sent in call to `_to` + */ + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) public virtual onlyApprovedOrOwner(tokenId) { + _transferChild( + tokenId, + to, + destinationId, + childIndex, + childAddress, + childId, + isPending, + data + ); + } + + /** + * @notice Used to transfer a child token from a given parent token. + * @dev When transferring a child token, the owner of the token is set to `to`, or is not updated in the event of `to` + * being the `0x0` address. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits {ChildTransferred} event. + * @param tokenId ID of the parent token from which the child token is being transferred + * @param to Address to which to transfer the token to + * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) + * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or + * pending array) + * @param childAddress Address of the child token's collection smart contract. + * @param childId ID of the child token in its own collection smart contract. + * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the + * parent token (`true`) or in the active array (`false`) + * @param data Additional data with no specified format, sent in call to `_to` + */ + function _transferChild( + uint256 tokenId, + address to, + uint256 destinationId, // newParentId + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) internal virtual { + Child memory child; + if (isPending) { + child = pendingChildOf(tokenId, childIndex); + } else { + child = childOf(tokenId, childIndex); + } + _checkExpectedChild(child, childAddress, childId); + + _beforeTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + + if (isPending) { + _removeChildByIndex(_pendingChildren[tokenId], childIndex); + } else { + delete _childIsInActive[childAddress][childId]; + _removeChildByIndex(_activeChildren[tokenId], childIndex); + } + + if (to != address(0)) { + if (destinationId == 0) { + IERC721(childAddress).safeTransferFrom( + address(this), + to, + childId, + data + ); + } else { + // Destination is an NFT + INestable(child.contractAddress).nestTransferFrom( + address(this), + to, + child.tokenId, + destinationId, + data + ); + } + } + + emit ChildTransferred( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + _afterTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + } + + function _checkExpectedChild( + Child memory child, + address expectedAddress, + uint256 expectedId + ) private pure { + if ( + expectedAddress != child.contractAddress || + expectedId != child.tokenId + ) revert UnexpectedChildId(); + } + + //////////////////////////////////////// + // CHILD MANAGEMENT GETTERS + //////////////////////////////////////// + + /** + * @notice Used to retrieve the active child tokens of a given parent token. + * @dev Returns array of Child structs existing for parent token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the active child tokens + * @return struct[] An array of Child structs containing the parent token's active child tokens + */ + + function childrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory) { + Child[] memory children = _activeChildren[parentId]; + return children; + } + + /** + * @notice Used to retrieve the pending child tokens of a given parent token. + * @dev Returns array of pending Child structs existing for given parent. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the pending child tokens + * @return struct[] An array of Child structs containing the parent token's pending child tokens + */ + + function pendingChildrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory) { + Child[] memory pendingChildren = _pendingChildren[parentId]; + return pendingChildren; + } + + /** + * @notice Used to retrieve a specific active child token for a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the child is being retrieved + * @param index Index of the child token in the parent token's active child tokens array + * @return struct A Child struct containing data about the specified child + */ + function childOf( + uint256 parentId, + uint256 index + ) public view virtual returns (Child memory) { + if (childrenOf(parentId).length <= index) revert ChildIndexOutOfRange(); + Child memory child = _activeChildren[parentId][index]; + return child; + } + + /** + * @notice Used to retrieve a specific pending child token from a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the pending child token is being retrieved + * @param index Index of the child token in the parent token's pending child tokens array + * @return struct A Child struct containting data about the specified child + */ + function pendingChildOf( + uint256 parentId, + uint256 index + ) public view virtual returns (Child memory) { + if (pendingChildrenOf(parentId).length <= index) + revert PendingChildIndexOutOfRange(); + Child memory child = _pendingChildren[parentId][index]; + return child; + } + + /** + * @notice Used to verify that the given child tokwn is included in an active array of a token. + * @param childAddress Address of the given token's collection smart contract + * @param childId ID of the child token being checked + * @return bool A boolean value signifying whether the given child token is included in an active child tokens array + * of a token (`true`) or not (`false`) + */ + function childIsInActive( + address childAddress, + uint256 childId + ) public view virtual returns (bool) { + return _childIsInActive[childAddress][childId] != 0; + } + + // HOOKS + + /** + * @notice Hook that is called before any token transfer. This includes minting and burning. + * @dev Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be transferred to `to`. + * - When `from` is zero, `tokenId` will be minted to `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` and `to` are never zero at the same time. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token is being transferred + * @param to Address to which the token is being transferred + * @param tokenId ID of the token being transferred + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called after any transfer of tokens. This includes minting and burning. + * @dev Calling conditions: + * + * - When `from` and `to` are both non-zero. + * - `from` and `to` are never zero at the same time. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token has been transferred + * @param to Address to which the token has been transferred + * @param tokenId ID of the token that has been transferred + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called before nested token transfer. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token is being transferred + * @param to Address to which the token is being transferred + * @param fromTokenId ID of the token from which the given token is being transferred + * @param toTokenId ID of the token to which the given token is being transferred + * @param tokenId ID of the token being transferred + */ + function _beforeNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called after nested token transfer. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token was transferred + * @param to Address to which the token was transferred + * @param fromTokenId ID of the token from which the given token was transferred + * @param toTokenId ID of the token to which the given token was transferred + * @param tokenId ID of the token that was transferred + */ + function _afterNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is added to the pending tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will receive a new pending child token + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _beforeAddChild( + uint256 tokenId, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called after a child is added to the pending tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has received a new pending child token + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _afterAddChild( + uint256 tokenId, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is accepted to the active tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param parentId ID of the token that will accept a pending child token + * @param childIndex Index of the child token to accept in the given parent token's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _beforeAcceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called after a child is accepted to the active tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param parentId ID of the token that has accepted a pending child token + * @param childIndex Index of the child token that was accpeted in the given parent token's pending children array + * @param childAddress Address of the collection smart contract of the child token that was expected to be located + * at the specified index of the given parent token's pending children array + * @param childId ID of the child token that was expected to be located at the specified index of the given parent + * token's pending children array + */ + function _afterAcceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is transferred from a given child token array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will transfer a child token + * @param childIndex Index of the child token that will be transferred from the given parent token's children array + * @param childAddress Address of the collection smart contract of the child token that is expected to be located + * at the specified index of the given parent token's children array + * @param childId ID of the child token that is expected to be located at the specified index of the given parent + * token's children array + * @param isPending A boolean value signifying whether the child token is being transferred from the pending child + * tokens array (`true`) or from the active child tokens array (`false`) + */ + function _beforeTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending + ) internal virtual {} + + /** + * @notice Hook that is called after a child is transferred from a given child token array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has transferred a child token + * @param childIndex Index of the child token that was transferred from the given parent token's children array + * @param childAddress Address of the collection smart contract of the child token that was expected to be located + * at the specified index of the given parent token's children array + * @param childId ID of the child token that was expected to be located at the specified index of the given parent + * token's children array + * @param isPending A boolean value signifying whether the child token was transferred from the pending child tokens + * array (`true`) or from the active child tokens array (`false`) + */ + function _afterTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending + ) internal virtual {} + + /** + * @notice Hook that is called before a pending child tokens array of a given token is cleared. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will reject all of the pending child tokens + */ + function _beforeRejectAllChildren(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called after a pending child tokens array of a given token is cleared. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has rejected all of the pending child tokens + */ + function _afterRejectAllChildren(uint256 tokenId) internal virtual {} + + // HELPERS + + /** + * @notice Used to remove a specified child token form an array using its index within said array. + * @dev The caller must ensure that the length of the array is valid compared to the index passed. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param array An array od Child struct containing info about the child tokens in a given child tokens array + * @param index An index of the child token to remove in the accompanying array + */ + function _removeChildByIndex(Child[] storage array, uint256 index) private { + array[index] = array[array.length - 1]; + array.pop(); + } +} diff --git a/assets/eip-6059/contracts/mocks/ERC721Mock.sol b/assets/eip-6059/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000000000..b31caeb545ac9e --- /dev/null +++ b/assets/eip-6059/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +/** + * @title ERC721Mock + * Used for tests with non RMRK implementer + */ +contract ERC721Mock is ERC721 { + constructor( + string memory name, + string memory symbol + ) ERC721(name, symbol) {} +} diff --git a/assets/eip-6059/contracts/mocks/NestableTokenMock.sol b/assets/eip-6059/contracts/mocks/NestableTokenMock.sol new file mode 100644 index 00000000000000..28861737fc1aac --- /dev/null +++ b/assets/eip-6059/contracts/mocks/NestableTokenMock.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "../NestableToken.sol"; + +//Minimal public implementation of IRMRKNestable for testing. +contract NestableTokenMock is NestableToken { + constructor() NestableToken() {} + + function mint(address to, uint256 tokenId) external { + _mint(to, tokenId); + } + + function nestMint( + address to, + uint256 tokenId, + uint256 destinationId + ) external { + _nestMint(to, tokenId, destinationId, ""); + } + + // Utility transfers: + + function transfer(address to, uint256 tokenId) public virtual { + transferFrom(_msgSender(), to, tokenId); + } + + function nestTransfer( + address to, + uint256 tokenId, + uint256 destinationId + ) public virtual { + nestTransferFrom(_msgSender(), to, tokenId, destinationId, ""); + } +} diff --git a/assets/eip-6059/hardhat.config.ts b/assets/eip-6059/hardhat.config.ts new file mode 100644 index 00000000000000..4289f15e8be78d --- /dev/null +++ b/assets/eip-6059/hardhat.config.ts @@ -0,0 +1,21 @@ +import { HardhatUserConfig } from 'hardhat/config'; +import '@nomicfoundation/hardhat-chai-matchers'; +import '@nomiclabs/hardhat-etherscan'; +import '@typechain/hardhat'; +import 'hardhat-contract-sizer'; +import 'hardhat-gas-reporter'; +import 'solidity-coverage'; + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.16', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, +}; + +export default config; diff --git a/assets/eip-6059/package.json b/assets/eip-6059/package.json new file mode 100644 index 00000000000000..90551fb11ea39a --- /dev/null +++ b/assets/eip-6059/package.json @@ -0,0 +1,41 @@ +{ + "name": "nestable-tokens", + "dependencies": { + "@openzeppelin/contracts": "^4.6.0" + }, + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^1.0.1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomiclabs/hardhat-ethers": "^2.2.1", + "@nomiclabs/hardhat-etherscan": "^3.1.0", + "@openzeppelin/test-helpers": "^0.5.15", + "@primitivefi/hardhat-dodoc": "^0.2.3", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.0", + "@types/node": "^18.0.3", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "chai": "^4.3.6", + "eslint": "^8.27.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.0.0", + "ethers": "^5.6.9", + "hardhat": "^2.12.2", + "hardhat-contract-sizer": "^2.6.1", + "hardhat-gas-reporter": "^1.0.8", + "prettier": "2.7.1", + "prettier-plugin-solidity": "^1.0.0-beta.20", + "solc": "^0.8.9", + "solhint": "^3.3.7", + "solidity-coverage": "^0.8.2", + "ts-node": "^10.8.2", + "typechain": "^8.1.0", + "typescript": "^4.7.4", + "walk-sync": "^3.0.0" + } +} diff --git a/assets/eip-6059/test/nestable.ts b/assets/eip-6059/test/nestable.ts new file mode 100644 index 00000000000000..be72822b2f3874 --- /dev/null +++ b/assets/eip-6059/test/nestable.ts @@ -0,0 +1,1158 @@ +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { BigNumber, constants } from 'ethers'; +import { NestableTokenMock } from '../typechain-types'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} + +const ADDRESS_ZERO = constants.AddressZero; + +async function parentChildFixture(): Promise<{ + parent: NestableTokenMock; + child: NestableTokenMock; +}> { + const factory = await ethers.getContractFactory('NestableTokenMock'); + + const parent = await factory.deploy(); + await parent.deployed(); + const child = await factory.deploy(); + await child.deployed(); + return { parent, child }; +} + +describe('NestableToken', function () { + let parent: NestableTokenMock; + let child: NestableTokenMock; + let owner: SignerWithAddress; + let tokenOwner: SignerWithAddress; + let addrs: SignerWithAddress[]; + + beforeEach(async function () { + [owner, tokenOwner, ...addrs] = await ethers.getSigners(); + ({ parent, child } = await loadFixture(parentChildFixture)); + }); + + describe('Minting', async function () { + it('cannot mint id 0', async function () { + const tokenId1 = 0; + await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError( + child, + 'IdZeroForbidden', + ); + }); + + it('cannot nest mint id 0', async function () { + const parentId = 1; + await child.mint(owner.address, parentId); + const childId1 = 0; + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'IdZeroForbidden'); + }); + + it('cannot mint already minted token', async function () { + const tokenId1 = 1; + await child.mint(owner.address, tokenId1); + await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError( + child, + 'ERC721TokenAlreadyMinted', + ); + }); + + it('cannot nest mint already minted token', async function () { + const parentId = 1; + const childId1 = 99; + await parent.mint(owner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted'); + }); + + it('cannot nest mint already minted token', async function () { + const parentId = 1; + const childId1 = 99; + await parent.mint(owner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted'); + }); + + it('can mint with no destination', async function () { + const tokenId1 = 1; + await child.mint(tokenOwner.address, tokenId1); + expect(await child.ownerOf(tokenId1)).to.equal(tokenOwner.address); + expect(await child.directOwnerOf(tokenId1)).to.eql([tokenOwner.address, bn(0), false]); + }); + + it('has right owners', async function () { + const otherOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(tokenOwner.address, tokenId1); + const tokenId2 = 2; + await parent.mint(otherOwner.address, tokenId2); + const tokenId3 = 3; + await parent.mint(otherOwner.address, tokenId3); + + expect(await parent.ownerOf(tokenId1)).to.equal(tokenOwner.address); + expect(await parent.ownerOf(tokenId2)).to.equal(otherOwner.address); + expect(await parent.ownerOf(tokenId3)).to.equal(otherOwner.address); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await parent.balanceOf(otherOwner.address)).to.equal(2); + + await expect(parent.ownerOf(9999)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + }); + + it('cannot mint to zero address', async function () { + await expect(child.mint(ADDRESS_ZERO, 1)).to.be.revertedWithCustomError( + child, + 'ERC721MintToTheZeroAddress', + ); + }); + + it('cannot nest mint to a non-contract destination', async function () { + await expect(child.nestMint(tokenOwner.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'IsNotContract', + ); + }); + + it('cannot nest mint to non nestable receiver', async function () { + const ERC721 = await ethers.getContractFactory('ERC721Mock'); + const nonReceiver = await ERC721.deploy('Non receiver', 'NR'); + await nonReceiver.deployed(); + + await expect(child.nestMint(nonReceiver.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'MintToNonNestableImplementer', + ); + }); + + it('cannot nest mint to a non-existent token', async function () { + await expect(child.nestMint(parent.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + }); + + it('cannot nest mint to zero address', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(child.nestMint(ADDRESS_ZERO, parentId, 1)).to.be.revertedWithCustomError( + child, + 'IsNotContract', + ); + }); + + it('can mint to contract and owners are ok', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // owner is the same adress + expect(await parent.ownerOf(parentId)).to.equal(tokenOwner.address); + expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + }); + + it('can mint to contract and direct owners are ok', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Direct owner is an address for the parent + expect(await parent.directOwnerOf(parentId)).to.eql([tokenOwner.address, bn(0), false]); + // Direct owner is a contract for the child + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + }); + + it("can mint to contract and parent's children are ok", async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + const children = await parent.childrenOf(parentId); + expect(children).to.eql([]); + + const pendingChildren = await parent.pendingChildrenOf(parentId); + expect(pendingChildren).to.eql([[bn(childId1), child.address]]); + expect(await parent.pendingChildOf(parentId, 0)).to.eql([bn(childId1), child.address]); + }); + + it('cannot get child out of index', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(parent.childOf(parentId, 0)).to.be.revertedWithCustomError( + parent, + 'ChildIndexOutOfRange', + ); + }); + + it('cannot get pending child out of index', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(parent.pendingChildOf(parentId, 0)).to.be.revertedWithCustomError( + parent, + 'PendingChildIndexOutOfRange', + ); + }); + + it('can mint multiple children', async function () { + const parentId = 1; + const childId1 = 99; + const childId2 = 100; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(parent.address, childId2, parentId); + + expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address); + expect(await child.ownerOf(childId2)).to.equal(tokenOwner.address); + + expect(await child.balanceOf(parent.address)).to.equal(2); + + const pendingChildren = await parent.pendingChildrenOf(parentId); + expect(pendingChildren).to.eql([ + [bn(childId1), child.address], + [bn(childId2), child.address], + ]); + }); + + it('can mint child into child', async function () { + const parentId = 1; + const childId1 = 99; + const granchildId = 999; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(child.address, granchildId, childId1); + + // Check balances -- yes, technically the counted balance indicates `child` owns an instance of itself + // and this is a little counterintuitive, but the root owner is the EOA. + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await child.balanceOf(child.address)).to.equal(1); + + const pendingChildrenOfChunky10 = await parent.pendingChildrenOf(parentId); + const pendingChildrenOfMonkey1 = await child.pendingChildrenOf(childId1); + + expect(pendingChildrenOfChunky10).to.eql([[bn(childId1), child.address]]); + expect(pendingChildrenOfMonkey1).to.eql([[bn(granchildId), child.address]]); + + expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]); + + expect(await child.ownerOf(granchildId)).to.eql(tokenOwner.address); + }); + + it('cannot have too many pending children', async () => { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + + // First 128 should be fine. + for (let i = 1; i <= 128; i++) { + await child.nestMint(parent.address, i, parentId); + } + + await expect(child.nestMint(parent.address, 129, parentId)).to.be.revertedWithCustomError( + child, + 'MaxPendingChildrenReached', + ); + }); + }); + + describe('Interface support', async function () { + it('can support IERC165', async function () { + expect(await parent.supportsInterface('0x01ffc9a7')).to.equal(true); + }); + + it('can support IERC721', async function () { + expect(await parent.supportsInterface('0x80ac58cd')).to.equal(true); + }); + + it('can support INestable', async function () { + expect(await parent.supportsInterface('0x42b0e56f')).to.equal(true); + }); + + it('cannot support other interfaceId', async function () { + expect(await parent.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('Adding child', async function () { + it('cannot add child from user address', async function () { + const tokenOwner1 = addrs[0]; + const tokenOwner2 = addrs[1]; + const parentId = 1; + await parent.mint(tokenOwner1.address, parentId); + const childId1 = 99; + await child.mint(tokenOwner2.address, childId1); + await expect(parent.addChild(parentId, childId1, '0x')).to.be.revertedWithCustomError( + parent, + 'IsNotContract', + ); + }); + }); + + describe('Accept child', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + }); + + it('can accept child', async function () { + await expect(parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1)) + .to.emit(parent, 'ChildAccepted') + .withArgs(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('can accept child if approved', async function () { + const approved = addrs[1]; + await parent.connect(tokenOwner).approve(approved.address, parentId); + await parent.connect(approved).acceptChild(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('can accept child if approved for all', async function () { + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator).acceptChild(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('cannot accept not owned child', async function () { + const notOwner = addrs[3]; + await expect( + parent.connect(notOwner).acceptChild(parentId, 0, child.address, childId1), + ).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot accept child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, otherChildId), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 0, otherAddress, childId1), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('cannot accept children for non existing index', async () => { + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 1, child.address, childId1), + ).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange'); + }); + + async function checkChildWasAccepted() { + expect(await parent.pendingChildrenOf(parentId)).to.eql([]); + expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + } + }); + + describe('Rejecting children', async function () { + let parentId: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, 99, parentId); + }); + + it('can reject all pending children', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + await expect(parent.connect(tokenOwner).rejectAllChildren(parentId, 3)) + .to.emit(parent, 'AllChildrenRejected') + .withArgs(parentId); + await checkNoChildrenNorPending(parentId); + + // They are still on the child + expect(await child.balanceOf(parent.address)).to.equal(3); + }); + + it('cannot reject all pending children if there are more than expected', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + await expect( + parent.connect(tokenOwner).rejectAllChildren(parentId, 1), + ).to.be.revertedWithCustomError(parent, 'UnexpectedNumberOfChildren'); + }); + + it('can reject all pending children if approved', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + const rejecter = addrs[1]; + await parent.connect(tokenOwner).approve(rejecter.address, parentId); + await parent.connect(rejecter).rejectAllChildren(parentId, 3); + await checkNoChildrenNorPending(parentId); + }); + + it('can reject all pending children if approved for all', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator).rejectAllChildren(parentId, 3); + await checkNoChildrenNorPending(parentId); + }); + + it('cannot reject all pending children for not owned pending child', async function () { + const notOwner = addrs[3]; + + await expect( + parent.connect(notOwner).rejectAllChildren(parentId, 2), + ).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner'); + }); + }); + + describe('Burning', async function () { + let parentId: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + }); + + it('can burn token', async function () { + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + await parent.connect(tokenOwner)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can burn token if approved', async function () { + const approved = addrs[1]; + await parent.connect(tokenOwner).approve(approved.address, parentId); + await parent.connect(approved)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can burn token if approved for all', async function () { + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can recursively burn nested token', async function () { + const childId1 = 99; + const granchildId = 999; + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(child.address, granchildId, childId1); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, granchildId); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await child.balanceOf(child.address)).to.equal(1); + + expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + expect(await child.childrenOf(childId1)).to.eql([[bn(granchildId), child.address]]); + expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]); + + // Sets recursive burns to 2 + await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(0); + expect(await child.balanceOf(parent.address)).to.equal(0); + expect(await child.balanceOf(child.address)).to.equal(0); + + await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + await expect(parent.directOwnerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + + await expect(child.ownerOf(childId1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + await expect(child.directOwnerOf(childId1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + + await expect(parent.ownerOf(granchildId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + await expect(parent.directOwnerOf(granchildId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + }); + + it('can recursively burn nested token with the right number of recursive burns', async function () { + // Parent + // -> Child1 + // -> GrandChild1 + // -> GrandChild2 + // -> GreatGrandChild1 + // -> Child2 + // Total tree 5 (4 recursive burns) + const childId1 = 99; + const childId2 = 100; + const grandChild1 = 999; + const grandChild2 = 1000; + const greatGrandChild1 = 9999; + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(parent.address, childId2, parentId); + await child.nestMint(child.address, grandChild1, childId1); + await child.nestMint(child.address, grandChild2, childId1); + await child.nestMint(child.address, greatGrandChild1, grandChild2); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId2); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild1); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild2); + await child.connect(tokenOwner).acceptChild(grandChild2, 0, child.address, greatGrandChild1); + + // 0 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 0)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, childId1); + // 1 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 1)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, grandChild1); + // 2 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, grandChild2); + // 3 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 3)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, greatGrandChild1); + // 4 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 4)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, childId2); + // 5 is just enough + await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 5); + }); + + async function checkBurntParent() { + expect(await parent.balanceOf(addrs[1].address)).to.equal(0); + await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + } + }); + + describe('Transferring Active Children', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + childId1 = 99; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + }); + + it('can transfer child with to as root owner', async function () { + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, false, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + await checkChildMovedToRootOwner(); + }); + + it('can transfer child to another address', async function () { + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, false, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + await checkChildMovedToRootOwner(toOwnerAddress); + }); + + it('can transfer child to another NFT', async function () { + const newOwnerAddress = addrs[2].address; + const newParentId = 2; + await parent.mint(newOwnerAddress, newParentId); + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + parent.address, + newParentId, + 0, + child.address, + childId1, + false, + '0x', + ), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]); + expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot transfer child out of index', async function () { + const toOwnerAddress = addrs[2].address; + const badIndex = 2; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, badIndex, child.address, childId1, false, '0x'), + ).to.be.revertedWithCustomError(parent, 'ChildIndexOutOfRange'); + }); + + it('cannot transfer child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, false, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, false, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('can transfer child if approved', async function () { + const transferer = addrs[1]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).approve(transferer.address, parentId); + + await parent + .connect(transferer) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child if approved for all', async function () { + const operator = addrs[2]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + + await parent + .connect(operator) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child with grandchild and children are ok', async function () { + const toOwner = tokenOwner.address; + const grandchildId = 999; + await child.nestMint(child.address, grandchildId, childId1); + + // Transfer child from parent. + await parent + .connect(tokenOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]); + + // Grandchild is still owned by child + expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]); + }); + + it('cannot transfer child if not child root owner', async function () { + const toOwner = tokenOwner.address; + const notOwner = addrs[3]; + await expect( + parent.connect(notOwner).transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot transfer child from not existing parent', async function () { + const badChildId = 99; + const toOwner = tokenOwner.address; + await expect( + parent + .connect(tokenOwner) + .transferChild(badChildId, toOwner, 0, 0, child.address, childId1, false, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId'); + }); + + async function checkChildMovedToRootOwner(rootOwnerAddress?: string) { + if (rootOwnerAddress === undefined) { + rootOwnerAddress = tokenOwner.address; + } + expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]); + + // Transferring updates balances downstream + expect(await child.balanceOf(rootOwnerAddress)).to.equal(1); + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + } + }); + + describe('Transferring Pending Children', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + }); + + it('can transfer child with to as root owner', async function () { + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, true, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + await checkChildMovedToRootOwner(); + }); + + it('can transfer child to another address', async function () { + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, true, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + await checkChildMovedToRootOwner(toOwnerAddress); + }); + + it('can transfer child to another NFT', async function () { + const newOwnerAddress = addrs[2].address; + const newParentId = 2; + await parent.mint(newOwnerAddress, newParentId); + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + parent.address, + newParentId, + 0, + child.address, + childId1, + true, + '0x', + ), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]); + expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot transfer child out of index', async function () { + const toOwnerAddress = addrs[2].address; + const badIndex = 2; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, badIndex, child.address, childId1, true, '0x'), + ).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange'); + }); + + it('cannot transfer child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, true, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, true, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('can transfer child if approved', async function () { + const transferer = addrs[1]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).approve(transferer.address, parentId); + + await parent + .connect(transferer) + .transferChild(parentId, toOwner, 0, 0,child.address, childId1, true, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child if approved for all', async function () { + const operator = addrs[2]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + + await parent + .connect(operator) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child with grandchild and children are ok', async function () { + const toOwner = tokenOwner.address; + const grandchildId = 999; + await child.nestMint(child.address, grandchildId, childId1); + + // Transfer child from parent. + await parent + .connect(tokenOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]); + + // Grandchild is still owned by child + expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]); + }); + + it('cannot transfer child if not child root owner', async function () { + const toOwner = tokenOwner.address; + const notOwner = addrs[3]; + await expect( + parent.connect(notOwner).transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot transfer child from not existing parent', async function () { + const badChildId = 99; + const toOwner = tokenOwner.address; + await expect( + parent + .connect(tokenOwner) + .transferChild(badChildId, toOwner, 0, 0, child.address, childId1, true, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId'); + }); + + async function checkChildMovedToRootOwner(rootOwnerAddress?: string) { + if (rootOwnerAddress === undefined) { + rootOwnerAddress = tokenOwner.address; + } + expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]); + + // Transferring updates balances downstream + expect(await child.balanceOf(rootOwnerAddress)).to.equal(1); + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + } + }); + + describe('Transfer', async function () { + it('can transfer token', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await parent.connect(firstOwner).transfer(newOwner.address, tokenId1); + + // Balances and ownership are updated + expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address); + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(newOwner.address)).to.equal(1); + }); + + it('cannot transfer not owned token', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await expect( + parent.connect(newOwner).transfer(newOwner.address, tokenId1), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot transfer to address zero', async function () { + const firstOwner = addrs[1]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await expect( + parent.connect(firstOwner).transfer(ADDRESS_ZERO, tokenId1), + ).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress'); + }); + + it('can transfer token from approved address (not owner)', async function () { + const firstOwner = addrs[1]; + const approved = addrs[2]; + const newOwner = addrs[3]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + + await parent.connect(firstOwner).approve(approved.address, tokenId1); + await parent.connect(firstOwner).transfer(newOwner.address, tokenId1); + + expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address); + }); + + it('can transfer not nested token with child to address and owners/children are ok', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + await parent.connect(firstOwner).transfer(newOwner.address, parentId); + + // Balances and ownership are updated + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(newOwner.address)).to.equal(1); + + expect(await parent.ownerOf(parentId)).to.eql(newOwner.address); + expect(await parent.directOwnerOf(parentId)).to.eql([newOwner.address, bn(0), false]); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(newOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + + // Parent still has its children + expect(await parent.pendingChildrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot directly transfer nested child', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.connect(firstOwner).transfer(newOwner.address, childId1), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('can transfer parent token to token with same owner, family tree is ok', async function () { + const firstOwner = addrs[1]; + const grandParentId = 999; + await parent.mint(firstOwner.address, grandParentId); + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Check balances + expect(await parent.balanceOf(firstOwner.address)).to.equal(2); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // Transfers token parentId to (parent.address, token grandParentId) + await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId); + + // Balances unchanged since root owner is the same + expect(await parent.balanceOf(firstOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await parent.balanceOf(parent.address)).to.equal(1); + + // Parent is still owner of child + let expected = [bn(childId1), child.address]; + checkAcceptedAndPendingChildren(parent, parentId, [expected], []); + // Ownership: firstOwner > newGrandparent > parent > child + expected = [bn(parentId), parent.address]; + checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]); + }); + + it('can transfer parent token to token with different owner, family tree is ok', async function () { + const firstOwner = addrs[1]; + const otherOwner = addrs[2]; + const grandParentId = 999; + await parent.mint(otherOwner.address, grandParentId); + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Check balances + expect(await parent.balanceOf(otherOwner.address)).to.equal(1); + expect(await parent.balanceOf(firstOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // firstOwner calls parent to transfer parent token parent + await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId); + + // Balances update + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(parent.address)).to.equal(1); + expect(await parent.balanceOf(otherOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // Parent is still owner of child + let expected = [bn(childId1), child.address]; + checkAcceptedAndPendingChildren(parent, parentId, [expected], []); + // Ownership: firstOwner > newGrandparent > parent > child + expected = [bn(parentId), parent.address]; + checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]); + }); + }); + + describe('Nest Transfer', async function () { + let firstOwner: SignerWithAddress; + let parentId: number; + let childId1: number; + + beforeEach(async function () { + firstOwner = addrs[1]; + parentId = 1; + childId1 = 99; + await parent.mint(firstOwner.address, parentId); + await child.mint(firstOwner.address, childId1); + }); + + it('cannot nest tranfer from non immediate owner (owner of parent)', async function () { + const otherParentId = 2; + await parent.mint(firstOwner.address, otherParentId); + // We send it to the parent first + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + // We can no longer nest transfer it, even if we are the root owner: + await expect( + child.connect(firstOwner).nestTransfer(parent.address, childId1, otherParentId), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot nest tranfer to same NFT', async function () { + // We can no longer nest transfer it, even if we are the root owner: + await expect( + child.connect(firstOwner).nestTransfer(child.address, childId1, childId1), + ).to.be.revertedWithCustomError(child, 'NestableTransferToSelf'); + }); + + it('cannot nest tranfer a descendant same NFT', async function () { + // We can no longer nest transfer it, even if we are the root owner: + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + const grandChildId = 999; + await child.nestMint(child.address, grandChildId, childId1); + // Ownership is now parent->child->granChild + // Cannot send parent to grandChild + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, grandChildId), + ).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant'); + // Cannot send parent to child + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, childId1), + ).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant'); + }); + + it('cannot nest tranfer if ancestors tree is too deep', async function () { + let lastId = childId1; + for (let i = 101; i <= 200; i++) { + await child.nestMint(child.address, i, lastId); + lastId = i; + } + // Ownership is now parent->child->child->child->child...->lastChild + // Cannot send parent to lastChild + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, lastId), + ).to.be.revertedWithCustomError(child, 'NestableTooDeep'); + }); + + it('cannot nest tranfer if not owner', async function () { + const notOwner = addrs[3]; + await expect( + child.connect(notOwner).nestTransfer(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot nest tranfer to address 0', async function () { + await expect( + child.connect(firstOwner).nestTransfer(ADDRESS_ZERO, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress'); + }); + + it('cannot nest tranfer to a non contract', async function () { + const newOwner = addrs[2]; + await expect( + child.connect(firstOwner).nestTransfer(newOwner.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'IsNotContract'); + }); + + it('cannot nest tranfer to contract if it does implement INestable', async function () { + const ERC721 = await ethers.getContractFactory('ERC721Mock'); + const nonNestable = await ERC721.deploy('Non receiver', 'NR'); + await nonNestable.deployed(); + await expect( + child.connect(firstOwner).nestTransfer(nonNestable.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'NestableTransferToNonNestableImplementer'); + }); + + it('can nest tranfer to INestable contract', async function () { + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + expect(await child.ownerOf(childId1)).to.eql(firstOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + }); + + it('cannot nest tranfer to non existing parent token', async function () { + const notExistingParentId = 9999; + await expect( + child.connect(firstOwner).nestTransfer(parent.address, childId1, notExistingParentId), + ).to.be.revertedWithCustomError(parent, 'ERC721InvalidTokenId'); + }); + }); + + async function checkNoChildrenNorPending(parentId: number): Promise { + expect(await parent.pendingChildrenOf(parentId)).to.eql([]); + expect(await parent.childrenOf(parentId)).to.eql([]); + } + + async function checkAcceptedAndPendingChildren( + contract: NestableTokenMock, + tokenId1: number, + expectedAccepted: any[], + expectedPending: any[], + ) { + const accepted = await contract.childrenOf(tokenId1); + expect(accepted).to.eql(expectedAccepted); + + const pending = await contract.pendingChildrenOf(tokenId1); + expect(pending).to.eql(expectedPending); + } +}); From 9005f86e77b449dcd28a22b4ad000e0fe8b742be Mon Sep 17 00:00:00 2001 From: yaruno Date: Fri, 16 Dec 2022 12:04:43 +0100 Subject: [PATCH 044/274] Added security considerations and backwards compatibility texts (#6152) --- EIPS/eip-5023.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5023.md b/EIPS/eip-5023.md index 10d806b1573782..6925e4cf1d358b 100644 --- a/EIPS/eip-5023.md +++ b/EIPS/eip-5023.md @@ -74,7 +74,7 @@ These tree structures can be further aggregated and collapsed to network represe ## Backwards Compatibility -TBD +This proposal is backwards compatible with EIP-721 and EIP-1155. ## Reference Implementation @@ -161,7 +161,8 @@ contract ShareableERC721 is ERC721URIStorage, Ownable, IERC5023 /* EIP165 */ { ## Security Considerations -Needs discussion. +Reference implementation should not be used as is in production. +There are no other security considerations related directly to implementation of this standard. ## Copyright From 41411befcda6363f4b1b7f5ccb49e44d1deb3888 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 16 Dec 2022 14:34:53 +0100 Subject: [PATCH 045/274] Replace mermaid with images (#6154) Replaced Mermaid diagrams with images, because the diagrams werent being rendered in the official collection of EIPs. --- EIPS/eip-6059.md | 36 +++--------------- .../eip-6059/img/eip-6059-abandon-child.png | Bin 0 -> 9616 bytes .../eip-6059/img/eip-6059-nestable-tokens.png | Bin 0 -> 20693 bytes assets/eip-6059/img/eip-6059-reject-child.png | Bin 0 -> 9497 bytes .../img/eip-6059-transfer-child-to-eoa.png | Bin 0 -> 12219 bytes .../img/eip-6059-transfer-child-to-token.png | Bin 0 -> 14063 bytes assets/eip-6059/img/eip-6059-unnest-child.png | Bin 0 -> 8761 bytes 7 files changed, 6 insertions(+), 30 deletions(-) create mode 100644 assets/eip-6059/img/eip-6059-abandon-child.png create mode 100644 assets/eip-6059/img/eip-6059-nestable-tokens.png create mode 100644 assets/eip-6059/img/eip-6059-reject-child.png create mode 100644 assets/eip-6059/img/eip-6059-transfer-child-to-eoa.png create mode 100644 assets/eip-6059/img/eip-6059-transfer-child-to-token.png create mode 100644 assets/eip-6059/img/eip-6059-unnest-child.png diff --git a/EIPS/eip-6059.md b/EIPS/eip-6059.md index fe98f0b0fd8a51..4a6692e713cc0c 100644 --- a/EIPS/eip-6059.md +++ b/EIPS/eip-6059.md @@ -21,16 +21,7 @@ The process of nesting an NFT into another is functionally identical to sending An NFT can be owned by a single other NFT, but can in turn have a number of NFTs that it owns. This proposal establishes the framework for the parent-child relationships of NFTs. A parent token is the one that owns another token. A child token is a token that is owned by another token. A token can be both a parent and child at the same time. Child tokens of a given token can be fully managed by the parent token's owner, but can be proposed by anyone. -```mermaid -graph LR - Z(EOA owning parent NFT) --> A[Parent NFT] - A --> B[Child NFT] - A --> C[Child NFT] - A --> D[Child NFT] - C --> E[Child's child NFT] - C --> F[Child's child NFT] - C --> G[Child's child NFT] -``` +![Nestable tokens](../assets/eip-6059/img/eip-6059-nestable-tokens.png) The graph illustrates how a child token can also be a parent token, but both are still administered by the root parent token's owner. @@ -465,38 +456,23 @@ Based on the desired state transitions, the values of these parameters have to b 1. **Reject child token** -```mermaid -graph LR - A(to = 0x0, isPending = true, destinationId = 0) -->|transferChild| B[Rejected child token] -``` +![Reject child token](../assets/eip-6059/img/eip-6059-reject-child.png) 2. **Abandon child token** -```mermaid -graph LR - A(to = 0x0, isPending = false, destinationId = 0) -->|transferChild| B[Abandoned child token] -``` +![Abandon child token](../assets/eip-6059/img/eip-6059-abandon-child.png) 3. **Unnest child token** -```mermaid -graph LR - A(to = rootOwner, destinationId = 0) -->|transferChild| B[Unnested child token] -``` +![Unnest child token](../assets/eip-6059/img/eip-6059-unnest-child.png) 4. **Transfer the child token to an EOA or an `ERC721Receiver`** -```mermaid -graph LR - A(to = newEoAToReceiveTheToken, destinationId = 0) -->|transferChild| B[Transferred child token to EOA or ERC721Receiver] -``` +![Transfer child token to EOA](../assets/eip-6059/img/eip-6059-transfer-child-to-eoa.png) 5. **Transfer the child token into a new parent token** -```mermaid -graph LR - A(to = collectionSmartContractOfNewParent, destinationId = IdOfNewParentToken) -->|transferChild| B[Transferred child token in a new parent token's pending array] -``` +![Transfer child token to parent token](../assets/eip-6059/img/eip-6059-transfer-child-to-token.png) This state change places the token in the pending array of the new parent token. The child token still needs to be accepted by the new parent token's root owner in order to be placed into the active array of that token. diff --git a/assets/eip-6059/img/eip-6059-abandon-child.png b/assets/eip-6059/img/eip-6059-abandon-child.png new file mode 100644 index 0000000000000000000000000000000000000000..e97465dbca45f132f9371eab01fbde41a08ef657 GIT binary patch literal 9616 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 R4)_!z=dJ8dF8B literal 0 HcmV?d00001 diff --git a/assets/eip-6059/img/eip-6059-reject-child.png b/assets/eip-6059/img/eip-6059-reject-child.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc8979906f210bce8ffb413395cd6372fc56ab8 GIT binary patch literal 9497 zcmbW7WmuG5`|igF6eJaqZls4s5D*aQ?k?$)ZWusOL>^jV2mt}<7=~^TkPhiC>FyYE zFW&#V_ow}3@8d9rVdlPPuC>;Ao#$_@P*r6aTr4sy2n2#FCo8EAf&2ppj|DIvfq(V( zO|`*)58Tvc-a<+SC^jJw8i<^vxTg2_-8mn_{;9i`!---|LROZidT%9}J9MqSBqBMx z6Nfm>ZB;9{6)jjkg-VuBR(FTfnwQ~~#rj*5n42(uQ_q*&Zfq92^prcF-)rRHc1w#2 z>6ADR5M6iMt4$vi+#l2vB}Ny2^8fzl0zcNK?C=^XKrWj-d`6IWTF3JOGw47$<{P_| zYo_{_TV#fsM_I3saBKZ;2zT{-Y!_A?+(Q&y{CU#no7r{V2lcm28KK6S zQrK-R_IUkS%7^r-TK9xKOL6uaX7ITGTIO@U*u%AFABQc>5 z#h=9SLJ=|{AIFq%aZcy&Xb&#$;ynUD)q4KzSsc!znfTpa^xZV#5hGcDKOwsK1;Xwy zG&|Jgp$MXEAcO|ufC&6lmvJ z&~&tc+l^|@kAAp@B}wV6OFTi(D9Xd(0MGtsl%CPmfIHkjc9%wn2ho6QVFnF=ukb&= z{zG~gt$klTRdsdsXie5st#Q-^B1S&js89#dR9ggbxVpN!rxr=`I~w*9o1A3oAgV4<{=a^Qt@BOa{rYsJn6{&mF1ky~^@BHzNGdud zrBKV5j_&Q@DW&F?r;Cf<#&9-+QC-DP@ID;MY53Q#s*Vdy8DlOlk>;I@b?&xu@;&qr z@YKh8Kbc4>k%Q@|;)tRmCd1}{?4t6F7s6+@Jx9f`R?A)AQ zqi?11aD(qDD=jT84hh$XfSXHhv-YoMw)o(O=WN<}islm~MmQ9&qJRE;;UnsIvLOzG zHT2wg?#;?YQHjVZDfyq`XPPf}MnzXsiug7RmYOwoT?O)|=^3}{%+Os`)Akj6Tee-DzF;eBg4K;oZCt{yB-%vG46Y=X==?-`eyLeN4 zQ(j)4^vZ?s@X(8duPX&#bfYgvX$WdWmai4Ss>zk{_TjV2>izLzPAV#szZ;X^B|^^r z?%zGsDKQ-0nw;k0;%XVKpu4;%b#v3R&gy&dV%Gd%ao(`rv$oyu%NOay z#Kg?p+?gwBOUv83NSy@ZN<5ztn7{v2rM-@xwZz`uTzy{>FNTbaOwgxKo0G_8wp^dB zQI@9LOZyU&=E%ZAwno469OYD@BN{cMFJHcZ>&11Wwsv-4EmKpds<5!$cSgAmR#pSi zv{}wx=RegMc*jeOx@T)M$g*i;WVv1ze~pPjDj%Qmw?CZzQ?WbV9+-hUVtW#uVo)WL zpq?M+v7NawiDWbHP<-&is6G;hbU1`kn4ia@D^GpomGe8M(9p+FgQ~RTm-cAU>FF}> zcNiNQu31c)8l-*5UfMNC&v@<4#xg1;qb=_3b?2#3wYL{p4gVAnaB8;dpDxm`T#X-* zQwa=DOQQmlWLUf;t+!VLL9E9Z3qGB@?@qP2xVR`!lH!tINr{n*$8N!~IV;=oghxYJ zW|+{5l7^S&s*(}@`ar6Li3zod$$}2vUke`6q@<+26oIG$twMsQPd|d6#y34S{(Quv z6lzdUGY*!d%g2=z6c!GVfAQ@Puk}FDuv8rpQ39Bpinr&Z`Q~54W!pm^Tr^#YA#u|8rKiL3H{Gp61&wUf#8z-A^m`8!di+|@qJKgJ;<&7z4bkyMb$qtI8Ed-;~X#tOn&jyMQeJ~$*Ct+qT;I`cHJel7i!!zI!?v|Xo zI$dOBf1&`wxyEX)j=AbRDuPLcCr<}K`{_}{PpPnT!abARNRjbz{ufMQ=vaPQS(!wo zfq_s#LBYxC>7nL38X@-!bmko%F>hz$o68$&H8pHmCI8Qjjc6Y~#)kQ)kqeZR*$h#X z7}bXd2QySvc^;j6?oRj2){a0hu?;?yyV*O=*7Qsw+Y&`@FtkgqV+^WGOf43qB}HPN!nMHTq_Fgu0h)WVlCLqoAjz zD;PUhuhvzx+R>=3uPw5H%m3g>Lz!EfnH zdX^~oO!T&fX}Kd9%#ohGxj;y%!;A|K4h|;n4VHm{FnBbHG^h9R@o6|W_Pf4jd}n5c z)iRK3Fj;ph0Ay7hb@bfX#r9Lw;H$pAKCOh>%w_S-3E$=b%7f#SuA!_eL7yY8=d6jt zSs}t{X>D2wn;+5YbsK$I?as2%JTHD&D8vO(UuMaW6!peZ5j$R21^P06|`uwS~ z9bx?Y_ph^?oB3FQ*3X z4L;@9Fs+rf{uHQQ+2`#Egt?<*WO4B>8%%+rv4p1lYiukwlQIRJpx{&NFxKpGC%(=N~EUY6J_xA$_OCNzkAh?QW zs-2#Yy!t$l>i2hDN7tHxk+IaG`}IJY$j6wNr{FW=kn;Td2$u{kZ;WvxXn?L>yLftO zZ|`x7zyhpuwzk)HtbkIL#2rk;Ah6h;F$MI7PzUrCXoY=^yp_`l!Jy$j6opjUjj^2h zjunjEFQ1N_N1q<~lw0;XfwJS~Yl20XF*!NO<6Vjm!Ka$ls1bYPT4r_J^A8%6xzmE` z?Rl*m)BU?9AU_I66{!`=DZzk|L2VK2Rt$iOcj6 z#00j~52}v3K5nH76ZIF()yUmrIC}Q1Jh{Ccy$Q7@41;yJZ2X}bFY+-qy}db?dhcMr zyt=A#zIXE~;QGh)BeXa;SmF78#WIqgRT0J#Ykhjsxyo~>RXD@P+R8&-iiH&W3 zanQPW(-Y4IG?~YJL$%Ixx94!V6SY^r|L?BX>6Vt6nc2blx%JUX_X{Q_UMJ%v!v^cY zG;T1_ad2@DkB_rFw#R?|{0Ujy+?2aI+vVosdIw%xQ%^h)p`fH>Ia}jmVP(YwoZ1&$ za%p2@D$rDC8~tU}vmX@xtpC}nUnr!r|ZkQMy z7>EqUCU)IaU#iG7ZuDK++S0cpJ3Ks8Oyv3)7f1LApGvVg;Kq37`#5osVk#b745s^v zdS-Z7zxcAhyE`~K8k3CQ9h#tQdrH{C*$k!SaTNlc1RY(mb#5*e1AkAW#85Ea<)e_T zz1tiyAt9HC9St{kq3y&#JkmUOxXGUhxS+76rY2bZOMC4K(i%gWLq z3^R_FYlG?ih-RXIoT04dj>*YH&;_;jvWi6e2L>3KyHbwVRsOE7Ml}a=v9NTbKlEW! zJl-6ETJ>ROdOG+uG-A)x#l^{llk$f5^|^zY0Oqm^D3gi*DFaa1xP1ftlpoka+qHZG z)jxbsTkC}}LKrnm4822GV!JOkb06~A4jaxhS2^wX#4?dypUtpdo>9sMV?CGu>GikW z^sPiE>+A6%y)c{czU=nx{QTfB2IDj)Qwxh1>HAA-{LyF_W~jqXuIZ}XmUuS!#bKuj z7aw0-ZiY?Z8%vtJuVKAcz%?aM*V{ah-k(RanVpm~rE$Ky6-!LkenCr{2CU*YI6S1# zUI34JzB5~4{Q|XKteLO={6i#V{n5^p2ET*8!bFMximhS_5eqfe=E%BR0!PFPdiqrP zc-HdwQ|B9-n}>5=YDZ}-A|fKFx$JnYmtdi4U-*Dp^zOTd;GO=Ss4nHPS(uN%@72~i z@Rk-i2;cjSuOTu5%4x3^v!s=t=MX>Pl3Z%|m7|pU7~iDGIUT5<`MY-?f`hZKIgX_w z$X;4*{24lGK0ZEvo^wp6)T2$A9nam;8tk?)-$AFnJdVqqSoBM(J z@)1y`C!iB=j29zd!hU`jKb$XH!z@*so6tNxJrq`Qfsms=RFa{K`BR!fAA-NX+?QmI zLrn#F^X82+a!DNx4Gq{`Ib~%$FsJ2vuUD6CU5mXohh^r&z#>Kie#&L7k6XXm(S+^} zNEg(YLVP5L9XJ~rCmI5RzqLNc$;10g9%Hmy(I48Uru53?oXDojtyP3|baYNmPq_sI zI){dk?fiIncsNvsudNPGPuUOs5eC)DxBkSBA3y#z+i-BbJ6A8z3+H5_r~gz`1O=(j z1~mClR9pnPz}m^?i0li3=?Yu=K>yw8%EY83Znrf>pD0t%MoC3suaHP&IE5eqGsWNY z^YdKOwsl-TS63cwZSCLxpjjFk8Ikf@;XoXwD@+d-e^Cgze+5qaos*MFv3{kzlKE;+ zJQ!JSuRTK_m3&D$)1}s62-K)z~ZD=*}+ zH85ckNWZW)qS73FXQF2KLwTCrplWQZPR!kg6I{xwFOh|H+tFl!=H7Fudh?P#;ra~f zc<>7&RlZ{GUl2-zcV`5+oEN!J?nzN%d)?`QX=-)&%ua1*dvnULYf8`vB#+mMKeRqA z1jf4DR>M6sCbg?;^R>^nUprG_%sM3$5TJ*hv$H{ZR7nLzP-JoGd2zwEjBRub;d9fD{EYwb>4VBt${RSXl=_~T;?@4 zl@M~8KgJ#fBE;EzlIt*oCn@>)TYCE7;;+Uq2NP9Q)l9aT$qQ72Drj#Ib@gxDe0+bw ztUcM8GNC$w*m2IPC2+iPJ(92+^)`0V0n6Cj+?mkWXUiNo!c=P+S?yqk`|R zeROxr*p)mzJ@YNE50=nwQS1DVA0O^aB9W40-QD;gE_K}AAQh8WLaNVo08`@%W8(C^PX$c|s-W9<%r>>^4p==iYY^`Gy2IyfcB_V6r(- zth>9Lgp~AV|4O;Dr$@di6?u3ZOeuxP1@6?^S|&?6;%j8&y72I3j&r>KF&QgK!TUr4N=uZ>!_`I?bad`XsI-E`KP^8>)j61%XcD0*lod+ql~*UUD@mH8~MY6Hd&CiFt$q=O_GW4m+Wo3hV#c6O#8q-r@Fn|;6xUO2C?9jOVp z5u&1^;DE?BVk!01KGzEDizkI2ho)-KZh)e%8XD(neWN@1nSfOvyY zwcmJ7CGiD^xfa?>uC zqZS_I>1+A;*ruFiNccVIM_g4@5Ef|W7ig<^#blxu)$=dceQ%fsl5uay0i>wt{nw~wBj}vpIhG$LA+DfZ0RS_7iHB(G zgXw0|6)r%%c_yvM}}vU%f(Y z2NxHg^a%2aGI!e-+S>fy-gyCMSvj}Y$56m_ELuO4y6@=LY+c>n1QI$L=qK6x6~lA# z6h}rz_@?(g#%Z%-KJM&Hl_NZK8$fgwSI@8D%?lJUZ19dbULWKQxb{XBY+jx3+r!>J zd-hB^iYlVMUU(!|#rpDO6EGMX0AEeEj|k07 z8|;jtW}CBW|N690LPFx@ElS4PdL57XF^473lo1Wk?s7E=vFm<(Or45Uo{o-H7f;6s z07t+VzzHKB{5^&8he8W%jGFT`RFaa39Ags_)a^c?{t6Z-Pfr`4gDfkjq%>qh?Enlw zZ2YO@d`4zI95KbJRdjK+AI4C{2A%m!FWdG7bqE?JiD-k^&=A{d+fhzVd{>uuQJImC zhWlkHBGWjFIHVeokM`;5FB-X#gfsjcj`$rN9rAuv-@kv?Yj~x<)cgonetRs;GP0}OdTS7^Q1#bm@IqIRs*aA1 zMh7d%C@52gs`uagRA+ho`26Bzg!AiH-JG&qhvA=kSj`JabFC;wGIBV4FoG?jsYw*@ zKvqxX9*Z(F{F`y9DI&Gpa0dr_Qvixmg{ujLr2_(*VCk91k}4|rUgvjN^6>>nZrK}x z?mb^(UdF_zO@1ZnU;e!QYbi_wz7T)^=QsB;*x%t1F(V|Ralh77(JFzs@Z zlFTtNBQ7qsZ2%z%`kp}2(&*#nH|=TM1UnF4 z+87FYSPu3-fMF%#<#mgs-0rwWmD|qNkd773++(nrs^3q@gc2O*W@~&;37AxzB+A2c@lsj zs|mbICpgwK`MF!_9m~iLDwzNvHz47X%N@#q*s{*e|IWtsJA0qybF7`1qC?AQ^%86X ztUQRXv#fVCBHSLA;`YBheGB7SGNKYo0etH1jJ z5W~s#`meSAApq~9fgg?i_DxYT3HJ4dEoLwXJ;w&tKKWvZP9}a z7G7I>WPQs>l21vbAU)rP7Y|9vQ1CcEkQ;B*rm*FjJ3{vvo%%N>teTh z0aD|(j>pf>pH0Qd$%*geu<%aJfnJk zfPSI6r5|GGW&WC>m-Jmh{E^QOKyhG)!T12Z z5fE3)Y*$I7`W9Fz3DQ{~i7k9^UzWhDgHuf&hU25Rj%z z?F8DE^X|0CXz|PkKw@^(=`UT$2&*k;CmJ_R+(~5;_G|4vSQ)3GbtVs z)a$y6ldn~XH#8(h8{6&d>dI23u6)|G3PNwozt6>77h9C8oEF|hkX7E>V(H_9`)05r zcaKi7!P*1S(!+K%FCVsHVsN@Gz{t3!1NH|EcTV@_uAV-5axHJ$+}GQiJ~}E>q*wmE zC%&Il(6tEc+|FFFxw^R}@-m?V+vt0?qoSb%p{ZaSbH0zvP*KMSzd=K`9Q2EzE$1kZJL0iF*zE;)aAMFk(A2?`)qgJ@ne zyf{2O{0(qAKu~yETU(u-ooS%Za3KAF(I%L&HNth!E^k;)8JC-rip7W^Ec32LOC5lghYIDLM#k zj9gsN8yhxYvoAO{_8E}j7hoqgF_A>O#83_desKTA`9?o|>fEkv99EMR0G4WM=Aeco z%H>w^U8Sikd&Pa=Ycqn+{x%twck$^49vq@_FOMxP-Pump)M}~+4Q4(4{g%J~ zrJgKX6{vC)xxDAiM<+WU;qY`=KqgHCR7|JiQc&RdhHksCx?N8}PEHP(!;}6ScR1vO zChM+#qpwHAx*@;Qd_x=RcpX2kYA))d!1Y#T9?7jSKHJjL zk`e6HmtCXD(UJY-(qG49GxK+#OxWF*)zwv(^F5`1(6sQ^ui?NCMx+Pmx5!ZY2Sq61 z4iBrpf_YN|WOeuQqb2CaIJgnOHtl}*W5R5mQAlkmgSK}rFmN~ufTqaE!g5%7yOVLS zaJX-Cx={KMyA_0P)l1#@yG;GcEl^6gv;75eX=x$Ank#INpQ2%Gd3w0%b@%qt^Bj=} zeQF~VJu!KLzGUQnb7?t_xJv@jjMsL!$V_aL4Re~{S2j`8^{~k;=A#T%uuGWu>TsrP zpu_$i!oGdG_V2#Ylh1+AbtW8tj}u}Bae(6v*mNxyP6P|}2mHvgvSi#oj&0ceu?of9 z!P@_xkGP$YbpF4bb9nkI`+p8i{NOG4q>v9il=Hn!xl@zBKQu8~&V(W%kyY;%q6Wt* zETt@+Sz?^`h8nGFomhFnGy8QF?G!H_`L6f;pBLE1tKPYLT8Z1Woz1W%WfrK>x^-`88h?CevD%7SJZIK?q9SUaVTR~#4}3#Xr3 zdS=>;C_1Lc3{usW_y6kKf`4YchGDqvMk7Q!T~uINW!Zm})A9U-tueIkG5EH-KOdGg zjJAJ|TM0}{T3PSaCKq{0sx$6GFz=9$?~21gNigCWTs_iOpUwGOZJXE^*6dX3ce~+- zOC<)IUOH@>)tAk??x}qSj;s(zcUzzcNY5F3zhU1ljENbHqn^$?@Py8XR`EMrIGIL6akU$mQJO+yQE7%1VOsH8|m&2>6Wfpd>8@ zDIFo&gFxOwWF$mYT)!VJyXnZ^J|mpYmMs1hqxzLA7W*xMQ!Jt+(kqh8*dp#%sA1=9 zt6Wk)lQy~dd<2&)d1du{Un}a*iBhkIHsRwX<+|gQo|M)5TUS>B$K!?_&tuJ%tkE6M zBv?!_M6$sD-yf}zBo2DJ)U)Iga_s_G1foag#JAvv`tTt=e+>OD8xu-0S?qE;^6fH6 z&b=1-c772@DDT~rOo}3sZAFA%ELh{w9E#UcYR?IB%FB!9Zv_X}#>s5v4bWMl&C{LB zl9B~}D-)HGgt1@_5C4EdUkQuXe40^PdpTm4L48hKR9HP}k=&KOSyy}vJe zuGW3Tx%KAcwEQ$(QC7QA_Ynb1$!(JlXS#aos}G_}QWFj+rOr;{oO$7tWPuMZJRez9 z=m(fr>zzj8ZCCjiq4Reu6jm%}d*QhvD=s_#X|}#&_5%iV^xXpm78IOLzf@QGi2tAK z)ZVFRS?tplc$IWRf9lSWPM6ahIeDv{4~B?Hwn`J5C<6y4DOl5MT_}ew*6;JJFqoWf zps>2ti4VG5=U&+Srg8tfY6Ver@&CJoxl)NT|GT0}IRjw0|8rGAMVr$L@Y9Dnlh2=T zR8&<<9%uG$o`ZXgE+-}@Wp8Ho-h$h%N;3Yx-znaMd$uoz^oNNo`|sbs`#mO~wHE8j zy5_6;;Jp7HG{MfyEdN^2Q&c2VE4%6-^|3~P+}xLmIXb5FpSZd)wtpkb3GyZ(Y9uzuNa+}-Djr}Mg0R8;gUQ1+3CA@O2$Ds*Y7bJtT&co+6O0fyvh8SjemO- z{eS-WhlRb%`x3=>;LwOJ}KYdl?f)Rr*bhf;~V$# zAUza&-G7vOQq7 zRbYJjcUaahOHyK~yDP-y^!{Wr#e{$WZM@dTBa-l%p3kv-L%XfG7_~2&Si;(xa$-{W zhhdkL`TPW`a-M6)&=8TWZO-ZOGNIo+0p|d2Y3cD?RUau5(id8K`hZwP*+{~jU=qPQ z#D|f``vM+^zqCmVnxhO&9^5h;|GX{q3!Be>_1CbuJw0ezFV(;3i`IScQB8db!HWn& z!V%bK;AX+h+(W^^cOm)nhdVau3LPFkxV)w&q_kA%;x^0YSn^=T=GHc@)`tIm^--30 z+DoqVDD|?;>*ZrM4`8L62y0RaMn}#{vHyBT@03*rlu4LRG?tbJ@#bpFO3QLOx9G z<%Y(oCe+j=+m+02nIX>#dGdr2$ID0iugDV;RezM0!ob1ZI)A%axW7*#C;v$P)_?*Y zjR4+wb0#!WAgCvzrQB@v%kJJ@#A>_x-@U!CQr)H}E4p~QmF5nxGuF1Y0`*$lvo)*k z%of7J!ZcdkIj5$l*Von{U|gOzr!a5cyiu}|*3^8nzrVk_K7Q$lEh!ZjN@T z)e9}13r z2dbMJxvnHe1VhUOJV@P(7S}U|rIz%Ugy|%^i!Ko8>}K$X$!S*jJd=U*xv|&>-C~FH z#;C7PVXuxB!@-kL-<(sVaZQU`Tfa(Ten#{5_BdD?oh|J#>I(j~J5>Q0+2#$q73cir6)tXtQ<2U9HN-cGQGUGA#K~}Ly^H_7 z!t?G=`{$Bom| z)A-u(`?Gd$t!)}Atoy%_g5ihv_baDxT9HBCQf@(xSLFRe`g9_Rct5Uj^+sn5UAst1 z!h!0CuD$5Z!fFw9wz<5%#*$LNvoOzV^x$enN!iUh_~FBca*u0j38_r%Hzz&6uB~ne zwsGBhDH~L&N3(<@Y!>U|FVE7>eeP}Ez@y&4K7ZpNC0M{^761uOgU@&`y<85N=p7eeh@=n``52u;867Lj^m7#twIF6 zA0Sm`3jQ&pbUZw$rZf7DQ-lYz2rP@C5&RhXtCEPw*vm(;`9FWY2tkKi>kM+}9~}(> zB}^`5twX0=piED&F9KfyJpYwWVP5RMgadczMv*H+fvBR$Fcg zCjJ?E=x5+}Iez&uQy@|yi|t3g8N}mW^tGV-iwl*~+}xL7{9TFkJI|WZq|wn#ulb$X zz!o_>GbV68SqV%?&<1eiS5Ux9EuZGcWy5d^zM%d{gbca4-FbI&YsMW(D*Rfvan7j2 ze_D0`%`3b(Jc zq@MxA{9VW;0uY+S&-g!M#p14IbP`etwH*=iAEWmX=*$ zhk{r55E!|M6;J!Yn4(Wvt!hU4)P8V)t8=*N#-BbkEHRc0~b>UD4Y14cVW z?Rc4Vco2LSiGzntjc?Ca4b06^L1F*>dyAjQNd{ZtvtZukU&M_B&O3)9rhOd+irwyqrM>#f$d zyEFkewyzEjSXfwop`nPo`}-U1ez5(6gI)DsHd#ytUV(SU=C)n(Ehu;g_)H86KGWJ# zgM;hi9VY>YImF7!N-l#hu^=~t*8yI=Os^*ti^BeNP4u&wS=ZFmD?L3uIrXu{Iy(wl zT0~M|@4(VhCLJA}Q*fpY#(zrIdt5U?)Oo47tK+i5lZEr3{TS6luL`_Zks%xA8sQCkQPMN`wF$yVv z&FN~-{Wk@TXbz6ubf~Y~P#6Z2^g8b&F)U8}3=5ZSRCzgOLQ)doS>{AM<*>7*y8d<7 zMu+p8G1n)nMn?-7yGsq}5^j;jXB2#VsD_4QuJ>1*92^8crDKFrcq;C)<9x?-@g3AOkC#^oUzpZ#85kNRGZYs2kwpSQV%rUSrQb8~%#!V+uZ z*4^(T?F5U_41O4S`4T>HlcAxZbuj$jzuEf-?z(5Ed8w&Ug&$bp@tM9$iiv%nLk_KX zL}|(kBBrrOO-;=ah2_j!(gb`8yhOFRp+6RdcakeVxL91OSNh?^XUSnIHw=CCi^Z78 zQPz>VaJTFAF`AN+E%d$S8ZR+YCs0CTP<&546lG-zeRsd=9i0#CO-h&Bt*RX^x?nSE zsr32ZUI>2D(dl-S#jlaq6y1alCfy-kRGI2g-1qDk0T^Bd3x*M zH+JIna6G?HLG!j-I~4v49&|RrOkk zSX_>k{v98Mx}2D=6VAY;U5OKXirldGeN=3_o)G83eOuJQn81> zGn}4$dX^dt?m3jvgu2<+w0U^F%l@CR#zoYscDwI7Lun<~C#vW21hW1db z&2qyo62K3Qj~1TEn3>i0H_xt)tcJ3LaWyqlQv~L5a-yTtHMXL@d>VE|63miKDwggFjv6aU76com zFH+$(mo)lou`#rM8YE}%;UQi?fXz^E8*6aQPqHMg$#jbqbnnADmb&gcj3kWEs#w*Qk zKz90ugdpU}rTT;Uzqz?t8&2P6<(HI{xsb6QO^HZ@34ceqM|ZZq|RK? zx7itlB3JagqJq`=cqt44g9NgsW=l*&)NM<_#Dsn@QxcGrgeM>%U|?edRD)jFVozrShw z6X8E}Iz+lTdPo2cjlZFV1+0TZhj;`wPJ^99A+K3jtGC0;mw`VWc5Jba zq(8Y+g`sR5OMcmIUf522dh~&$5C7$Fd-I0$%ZVPFj*gT}wR|Rp^p&`R0_njFTzl6h zcUVjS&sVF|sfmeTKR+@^`%*J30ry{_#BF;X9(w}>phJzcvfQto+jmB9d7Ka524g&H zuKn28t}8XXJ#Tn%UE_SjFBx# zlSeUM*P|^g@a>N0U{qGJVPRv3!lBHxkBr1>wt>OVHBRGC=8uYhJ(mY!D-c>M#(o$& zO?=tv7c#f70EI?Zj>XZ@u@{s8EL&_4bo;;kfQNP6*fW6VF9 zSPx79jW+Lh$IG&Qzf3N9!pufVDBiYX2tNx1xxJwO!@HI$PXz%TL>8i1^Pg3ZSA?m#S5Fo&dwJyGBQBa0nSWQ^#Im%Y-*}IjngWH$H2}G z!^Oo#LRz}3D}b^lkHOdm++dJ8V2Lnfg}FPIZ6$; zYyMUX<=R*LBk|MsOe9nic`m4m8r44niXB!1$CrA;4aY<3G z50C2W7dxKC+I11{Qe@NYA_lcB<6TmdcDln10M8hD7*5Y+1x^=OOF75cHJq>M@J*k8_}7cSs?8qPQo9Yn1?j9X76oP5lms2-TfLL2 zihEDbe@=2UKGnOOX{Iuua5`pcNsWwiQh3F{C)6zhg+(cmc(LXn^iRCm2S=h@vg>&r zE061AyU!*jM*nt^JO^#8-N^yfJz0Hbv=?#ii_WNU*unB!eC0A;P^h*F0YEc0J}#-I zMh#g5Wd?Y*294V1Kw`4tYh?+2vuV+Q3X* zkR3mwr}KO}J*Cac*{a!v84lv|B023qTl>2Gmu&1%CE#~})|fWZU+s<~jE|eG9Un7R zrcbtb{Q0ClYd{wVscX^(c;;DB0AVqfa_U{J{Pix0VIJyPb@%B@0HHrqq8;gUfX9Ad z&qD-R8lrpm4hEbgCpP!Y?(PHNI`*BeKUDQ3COc&5AQ^q0!Y1a^Amidfe!R!%h$2+~ zq^0HW2iuS+Av2Q9LBM4_^sqXY!q1AqY*U%rmgjLDXJ|5=Sn&SE(g6`9Q^>1Zg&G9~ zrQ9?M1EAt9$@|>gC2S9LS4+Lk<}fwa+4@}Y+TXr=!ion+{vGsugdd784WuQCJu*8UQb< zsj2yyFD@?bbZ?R-l4N*wy12QSC`-u8=t)gcalHskf`eie)%%3rs3PN7073sQFK=s^ zM{=ZNzGY>_71`n4}_K_qk-eqYd?OB@fLtp@1Nwpma@1Z~8sjag@{!VkS zS^ynAV7$)m@*-oVq=7XsxfYN%ztv6Eb1UbZs4yhsfov`=!R#MDeCz99x1J?c=4#Jz zGQKZTGxgLh08s${-d3%RRcZ*Cmev9&@WY1@_*UIL+i6hRZ#s;6Ll@4OA?QKGtS8VD zj{AfyI2X^+64xEn**QvnA}KTA<{vD{%OBJ2566Cbx~^Mow~7R6L_3tfoHx~S)uEJf zlvGaKY$X2qNp2g@U6#dyabjR%VuILwHZ?UR8GRie{F9t8F)FER-%Y`|j- zi6Hi)5PAnS3?}cVn6CA$f=a?)6&lDkkbbpvOkeJqFdBY-6n3*{sONOEoxBGea*0o4 zAG6Dl)(YP6r*VwO{?Ey>6tc#?J49vv=XePz_`3V%>OCB(xJstUOe|((HO84*1meKm9EcvgWJHq zI=x;kg9>0!W<-FFN=o7ijf%q7NKK$#?ku$sCE%QRb>CP6d>X)@!T{?8Z|YZu04Xb` zIj#{E0A!*UBr^667uY;Pob2qx2J!1Xv~_H!ZX>yb74~MWyB| z&2UhJ%gqg}v(sl^?xC8eF1xSb*ij?U&t=nO&zT)xe=1+tY;cvoBp~pop>A+vCmBo{ z#=4omyMz(T|A#9ro-;07^7@WE-@O*_%t@<2?v$ttcEsh zG8c)vYhZMA^Pm$HFF3ArG+bO&5Jy0d4ZxfF`cs7yB0@r{O1AmjFeV^ry)(AKX^<1P zBLOyxxj9Gos!v;Iwm1wlg|N1EKg~ceJh`11GY>sHWbn)SB5W7<@Ssz$iZjzxN8x;Q zF0P@07#C;TKI?+8%0SZ3@0OuX7>^?@Mf`#jgRJM|nX%+ymxQ}(G~YHkphi2$KNZ#u-- zE=b+k8;jTp!}IRlJN@ePqWJiD1y$8VoVT|%_RmjVz?4J)_Me$4r`PjS%hbJ^x_Tcl z`B^QeXp6OKkc-qxIy(Xof`r^=$Hqjwy<7WTc#$Vdb@`Xtd`RnTm&LA+CtCD1w3?HK zo0|o)4}fW6=XWq$rtr;qab{-b@og^KpLP2ch(=~y95%-vCPysaME{xiWKcNzhlhKU z7)9kyV0D*Gjp*ZR!XgwoWeBcwshDEcRWoy=NSoEPY%`hNf|RtHZ;}8)n#y-ruHMHb zM7ie4o%v^EP%JG=Q&3WV+xwuo>Z!fp=u^-fFl?Hm+Z0{z!HpFZly&g0xzYQPZO)o& zXlMauZR8FEva%9?V12OM)OLFw$ed~`Q*CJi45#B1b~B{E!w!8Q88m6sN=vOVUeWth zwb;nAGeR8l^XJd@*;q(>H1R>ly2`YbsgdDcA|+F36TS)KPK5>oDgg%($jM)Xbcq5^ zmRee_-*iYDs*|s@J?(1c3}ZDRf#B|b_g=1fr0y8NUUp&O0LR&^sf9(9uP>(G^FlS2 zVDP5|Q0oD9#<{t-E9(pie*~%xX?|W40IzYQ6CVJjgIVT%$1k{jK;LRG;MuO0W>)8b zc(u+}Hy|j83WNY(o;-P=;grRsxAmfP=ql%1dxmfz|-XjO(t$=d>Rl7iXl?O4L>N5pXuhafR2-DO0A< zqwFr%OK51Hlfw=T4`BoFJmJh_Mw12l)4WMgP-Ma09OCAM27Hh;ehfj6tAM;bJnvf* z0x&SFSKTE7vR~1+WaQ

(%SBxo*y1A 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 literal 0 HcmV?d00001 diff --git a/assets/eip-6059/img/eip-6059-transfer-child-to-token.png b/assets/eip-6059/img/eip-6059-transfer-child-to-token.png new file mode 100644 index 0000000000000000000000000000000000000000..ee94447fcaef98ba846758cd5f0d2d621e4406f9 GIT binary patch literal 14063 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 literal 0 HcmV?d00001 diff --git a/assets/eip-6059/img/eip-6059-unnest-child.png b/assets/eip-6059/img/eip-6059-unnest-child.png new file mode 100644 index 0000000000000000000000000000000000000000..f675c9c28d59ab530960532d4564e8ca88479374 GIT binary patch literal 8761 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 Dm(GWM literal 0 HcmV?d00001 From e59b1dfc6ed42f36ba63f3e27f093535e3a3c5d9 Mon Sep 17 00:00:00 2001 From: Taehee Yoon Date: Sat, 17 Dec 2022 00:54:29 +0900 Subject: [PATCH 046/274] Update EIP-5192: Fix typo (#6148) --- EIPS/eip-5192.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5192.md b/EIPS/eip-5192.md index 3ed7d939cf2c90..30e5d147e25d5d 100755 --- a/EIPS/eip-5192.md +++ b/EIPS/eip-5192.md @@ -19,7 +19,7 @@ This standard is an extension of [EIP-721](./eip-721.md). It proposes a minimal The Ethereum community has expressed a need for non-transferrable, non-fungible, and socially-priced tokens similar to World of Warcraft’s soulbound items. But the lack of a token standard leads many developers to simply throw errors upon a user's invocation of transfer functionalities. Over the long term, this will lead to fragmentation and less composability. -In this document, we outline a minimal addition to [EIP-721](./eip-721.md) that allows wallet implementers to check for a token contract's permanent (non-)transferrability using [EIP-165](./eip-165.md). +In this document, we outline a minimal addition to [EIP-721](./eip-721.md) that allows wallet implementers to check for a token contract's permanent (non-)transferability using [EIP-165](./eip-165.md). ## Specification From bb8e8a7a5387d9a4e1493c1e6df00b3a4018b3dc Mon Sep 17 00:00:00 2001 From: yaruno Date: Fri, 16 Dec 2022 16:56:33 +0100 Subject: [PATCH 047/274] Requesting status change to last call, added last call deadline date (#6153) --- EIPS/eip-5023.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5023.md b/EIPS/eip-5023.md index 6925e4cf1d358b..50db6e7d30ef89 100644 --- a/EIPS/eip-5023.md +++ b/EIPS/eip-5023.md @@ -4,7 +4,8 @@ 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: Review +status: Last Call +last-call-deadline: 2022-12-31 type: Standards Track category: ERC created: 2022-01-28 From ee728c424afb78f04685f74768bd742fbf0c160a Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Fri, 16 Dec 2022 20:33:10 +0100 Subject: [PATCH 048/274] EIP-4938: Move to back to draft (#6014) * EIP-4938: Move to Last Call * EIP-4938: fix linting issues * eip-4938: move to draft * eip-4938: tag specific version --- EIPS/eip-4938.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-4938.md b/EIPS/eip-4938.md index 6194b86ffee24b..3867d780a38955 100644 --- a/EIPS/eip-4938.md +++ b/EIPS/eip-4938.md @@ -1,10 +1,10 @@ --- eip: 4938 -title: "eth/67: Removal of GetNodeData" +title: "eth/67 - Removal of GetNodeData" description: "Remove GetNodeData and NodeData messages from the wire protocol" author: Marius van der Wijden (@MariusVanDerWijden), Felix Lange , Gary Rong discussions-to: https://ethereum-magicians.org/t/eip-4938-removal-of-getnodedata/8893 -status: Stagnant +status: Draft type: Standards Track category: Networking created: 2022-03-23 @@ -13,7 +13,7 @@ requires: 2464, 2481 ## Abstract -The [Ethereum Wire Protocol](https://github.com/ethereum/devp2p/tree/master/caps/eth.md) defines request and response messages for exchanging data between clients. The `GetNodeData` request retrieves a set of trie nodes or contract code from the state trie by hash. We propose to remove the `GetNodeData` and `NodeData` messages from the wire protocol. +The [Ethereum Wire Protocol](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md) defines request and response messages for exchanging data between clients. The `GetNodeData` request retrieves a set of trie nodes or contract code from the state trie by hash. We propose to remove the `GetNodeData` and `NodeData` messages from the wire protocol. ## Motivation @@ -34,7 +34,7 @@ Remove the following message types from the `eth` protocol: ## Rationale -A replacement for `GetNodeData` is available in the [snap protocol](https://github.com/ethereum/devp2p/tree/master/caps/snap.md). Specifically, clients can use the [GetByteCodes](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#getbytecodes-0x04) and [GetTrieNodes](https://github.com/ethereum/devp2p/blob/master/caps/snap.md#gettrienodes-0x06) messages instead of `GetNodeData`. The snap protocol can be used to implement the "fast sync" algorithm, though it is recommended to use it for "snap sync". +A replacement for `GetNodeData` is available in the [snap protocol](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/snap.md). Specifically, clients can use the [GetByteCodes](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/snap.md#getbytecodes-0x04) and [GetTrieNodes](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/snap.md#gettrienodes-0x06) messages instead of `GetNodeData`. The snap protocol can be used to implement the "fast sync" algorithm, though it is recommended to use it for "snap sync". ## Backwards Compatibility @@ -47,5 +47,6 @@ This EIP does not change consensus rules of the EVM and does not require a hard None ## Copyright -Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). + +Copyright and related rights waived via [CC0](../LICENSE.md). From cdb4b438ea6dfda2916a22dddbb36180341e23e8 Mon Sep 17 00:00:00 2001 From: coderfengyun Date: Sat, 17 Dec 2022 03:50:25 +0800 Subject: [PATCH 049/274] Update EIP-5489: Move to final (#6062) --- EIPS/eip-5489.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/EIPS/eip-5489.md b/EIPS/eip-5489.md index ff9a5e9fa3f895..aec096b66bc551 100644 --- a/EIPS/eip-5489.md +++ b/EIPS/eip-5489.md @@ -4,8 +4,7 @@ title: NFT Hyperlink Extension description: NFT Hyperlink Extension embeds hyperlinks onto NFTs, allowing users to click any hNFT and be transported to any url set by the owner. author: IronMan_CH (@coderfengyun) discussions-to: https://ethereum-magicians.org/t/eip-5489-nft-hyperlink-extension/10431 -status: Last Call -last-call-deadline: 2022-11-29 +status: Final type: Standards Track category: ERC created: 2022-08-16 @@ -88,7 +87,7 @@ interface IERC5489 { /** * @dev Throws if `tokenId` is not a valid NFT. URIs are defined in RFC 3986. - * The URI MUST point to a JSON file that confirms to the "EIP5489 Metadata JSON schema". + * The URI MUST point to a JSON file that conforms to the "EIP5489 Metadata JSON schema". * * returns the latest uri of an slot on a token, which is indicated by `tokenId`, `slotManagerAddr` */ @@ -121,16 +120,6 @@ The `supportInterface` method MUST return true when called with `0x8f65987b`. The `authorizeSlotTo`, `revokeAuthorization`, and `revokeAllAuthorizations` functions are authenticated if and only if the message sender is the owner of the token. -## Rationale - -### Extends NFT with hyperlinks - -URIs are used to represent the value of slots to ensure enough flexibility to deal with different use cases. - -### Authorize slot to address - -We use addresses to represent the key of slots to ensure enough flexibility to deal with all use cases. - ### Metadata JSON schema ```json @@ -154,6 +143,16 @@ We use addresses to represent the key of slots to ensure enough flexibility to d } ``` +## Rationale + +### Extends NFT with hyperlinks + +URIs are used to represent the value of slots to ensure enough flexibility to deal with different use cases. + +### Authorize slot to address + +We use addresses to represent the key of slots to ensure enough flexibility to deal with all use cases. + ## Backwards Compatibility As mentioned in the specifications section, this standard can be fully EIP-721 compatible by adding an extension function set. From 1a99c770885da1bdc853f222c265f2d38acc8ff5 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Fri, 16 Dec 2022 14:07:46 -0700 Subject: [PATCH 050/274] eip-5757: move to final (#6033) --- EIPS/eip-5757.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-5757.md b/EIPS/eip-5757.md index b9a262997b2f26..373bd7ec801517 100644 --- a/EIPS/eip-5757.md +++ b/EIPS/eip-5757.md @@ -4,8 +4,7 @@ title: Process for Approving External Resources description: Requirements and process for allowing new origins of external resources author: Sam Wilson (@SamWilsn) discussions-to: https://ethereum-magicians.org/t/eip-5757-process-for-approving-external-resources/11215 -status: Last Call -last-call-deadline: 2022-11-16 +status: Final type: Meta created: 2022-09-30 requires: 1 From 146380b7a0d2d90918f8049598bc0e1bcf4a68ff Mon Sep 17 00:00:00 2001 From: Matt Solomon Date: Fri, 16 Dec 2022 13:44:37 -0800 Subject: [PATCH 051/274] Add EIP-5744: Latent Fungible Tokens (#5744) * Latent Fungible Tokens * update number * style: add solidity syntax highlighting * style: syntax highlighting * Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * update em dash character * change token name/symbol from should to must * cleanup and clarifications * Apply suggestions from code review Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Payom Dousti Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5744.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 EIPS/eip-5744.md diff --git a/EIPS/eip-5744.md b/EIPS/eip-5744.md new file mode 100644 index 00000000000000..19431e24941df5 --- /dev/null +++ b/EIPS/eip-5744.md @@ -0,0 +1,93 @@ +--- +eip: 5744 +title: Latent Fungible Token +description: An interface for tokens that become fungible after a period of time. +author: Cozy Finance (@cozyfinance), Tony Sheng (@tonysheng), Matt Solomon (@mds1), David Laprade (@davidlaprade), Payom Dousti (@payomdousti), Chad Fleming (@chad-js), Franz Chen (@Dendrimer) +discussions-to: https://ethereum-magicians.org/t/eip-5744-latent-fungible-token/11111 +status: Draft +type: Standards Track +category: ERC +created: 2022-09-29 +requires: 20, 2612 +--- + +## Abstract + +The following standard is an extension of [EIP-20](./eip-20.md) that enables tokens to become fungible after some initial non-fungible period. +Once minted, tokens are non-fungible until they reach maturity. +At maturity, they become fungible and can be transferred, traded, and used in any way that a standard EIP-20 token can be used. + +## Motivation + +Example use cases include: + +- Receipt tokens that do not become active until a certain date or condition is met. For example, this can be used to enforce minimum deposit durations in lending protocols. +- Vesting tokens that cannot be transferred or used until the vesting period has elapsed. + +## Specification + +All latent fungible tokens MUST implement EIP-20 to represent the token. +The `balanceOf` and `totalSupply` return quantities for all tokens, not just the matured, fungible tokens. +A new method called `balanceOfMatured` MUST be added to the ABI. +This method returns the balance of matured tokens for a given address: + +```solidity +function balanceOfMatured(address user) external view returns (uint256); +``` + +An additional method called `getMints` MUST be added, which returns an array of all mint metadata for a given address: + +```solidity +struct MintMetadata { + // Amount of tokens minted. + uint256 amount; + // Timestamp of the mint, in seconds. + uint256 time; + // Delay in seconds until these tokens mature and become fungible. When the + // delay is not known (e.g. if it's dependent on other factors aside from + // simply elapsed time), this value must be `type(uint256).max`. + uint256 delay; +} + +function getMints(address user) external view returns (MintMetadata[] memory); +``` + +Note that the implementation does not require that each of the above metadata parameters are stored as a `uint256`, just that they are returned as `uint256`. + +An additional method called `mints` MAY be added. +This method returns the metadata for a mint based on its ID: + +```solidity +function mints(address user, uint256 id) external view returns (MintMetadata memory); +``` + +The ID is not prescriptive—it may be an index in an array, or may be generated by other means. + +The `transfer` and `transferFrom` methods MAY be modified to revert when transferring tokens that have not matured. +Similarly, any methods that burn tokens MAY be modified to revert when burning tokens that have not matured. + +All latent fungible tokens MUST implement EIP-20’s optional metadata extensions. +The `name` and `symbol` functions MUST reflect the underlying token’s `name` and `symbol` in some way. + +## Rationale + +The `mints` method is optional because the ID is optional. In some use cases such as vesting where a user may have a maximum of one mint, an ID is not required. + +Similarly, vesting use cases may want to enforce non-transferrable tokens until maturity, whereas lending receipt tokens with a minimum deposit duration may want to support transfers at all times. + +It is possible that the number of mints held by a user is so large that it is impractical to return all of them in a single `eth_call`. +This is unlikely so it was not included in the spec. +If this is likely for a given use case, the implementer may choose to implement an alternative method that returns a subset of the mints, such as `getMints(address user, uint256 startId, uint256 endId)`. +However, if IDs are not sequential, a different signature may be required, and therefore this was not included in the specification. + +## Backwards Compatibility + +This proposal is fully backward compatible with the EIP-20 standard and has no known compatibility issues with other standards. + +## Security Considerations + +Iterating over large arrays of mints is not recommended, as this is very expensive and may cause the protocol, or just a user's interactions with it, to be stuck if this exceeds the block gas limit and reverts. There are some ways to mitigate this, with specifics dependent on the implementation. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 3be2d38989992d3f91424b025fd9fd23ceef8172 Mon Sep 17 00:00:00 2001 From: eth-bot <85952233+eth-bot@users.noreply.github.com> Date: Sat, 17 Dec 2022 16:15:50 -0800 Subject: [PATCH 052/274] (bot 1272989785) moving EIPS/eip-4760.md to stagnant (#5962) PR 5962 with changes to EIPS/eip-4760.md was created on (2022-Nov-15th@15.22.32) which is before the cutoff date of (2022-Dec-4th@00.15.49) i.e. 2 weeks ago --- EIPS/eip-4760.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4760.md b/EIPS/eip-4760.md index 3abf2e381c6bfe..aafcbfd365d511 100644 --- a/EIPS/eip-4760.md +++ b/EIPS/eip-4760.md @@ -4,7 +4,7 @@ title: SELFDESTRUCT bomb description: Deactivate SELFDESTRUCT by changing it to SENDALL and stage this via a stage of exponential gas cost increases. author: Guillaume Ballet (@gballet), Vitalik Buterin (@vbuterin), Dankrad Feist (@dankrad) discussions-to: https://ethereum-magicians.org/t/eip-4760-selfdestruct-bomb/8713 -status: Draft +status: Stagnant type: Standards Track category: Core created: 2022-02-03 From 4cf313025c4e822d0c5df24318b62a555e7ecbe5 Mon Sep 17 00:00:00 2001 From: Taehee Yoon Date: Mon, 19 Dec 2022 10:48:49 +0900 Subject: [PATCH 053/274] Update EIP-6093: Fix typos (#6149) --- EIPS/eip-6093.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-6093.md b/EIPS/eip-6093.md index 2f85e9aeb7858f..e47b3373fdb074 100644 --- a/EIPS/eip-6093.md +++ b/EIPS/eip-6093.md @@ -19,7 +19,7 @@ Ethereum applications and wallets have historically relied on revert reason stri ## Motivation -Since the introduction of Solidity custom errors in v0.8.4, these have provided a way to show failures in a more expresive and gas efficient manner with dynamic arguments, while reducing deployment costs. +Since the introduction of Solidity custom errors in v0.8.4, these have provided a way to show failures in a more expressive and gas efficient manner with dynamic arguments, while reducing deployment costs. However, [EIP-20](./eip-20.md), [EIP-721](./eip-721.md), [EIP-1155](./eip-1155.md) were already finalized when custom errors were released, so no errors are included in their specification. @@ -295,8 +295,8 @@ Given the above, we can summarize the construction of error names with a grammar Where: - _Domain_: `ERC20`, `ERC721` or `ERC1155`. Although other token standards may be suggested if not considered in this EIP. -- _ErrorPrefix_: `Invalid`, `Insufficient`, or another if it's more appropiated. -- _Subject_: `Sender`, `Receiver`, `Balance`, `Approver`, `Operator`, `Approval` or another if it's more appropiated, and must make adjustments based on domain's naming convention. +- _ErrorPrefix_: `Invalid`, `Insufficient`, or another if it's more appropriated. +- _Subject_: `Sender`, `Receiver`, `Balance`, `Approver`, `Operator`, `Approval` or another if it's more appropriated, and must make adjustments based on domain's naming convention. - _Arguments_: Follow the [_who_, _what_ and _why_ order](#arguments). ## Backwards Compatibility From 1377f1a1ba0ee0e29d965c01ed3d2575cb862536 Mon Sep 17 00:00:00 2001 From: Marius van der Wijden Date: Mon, 19 Dec 2022 09:58:32 +0100 Subject: [PATCH 054/274] Update EIP-2481: Move to final (#6015) * EIP-2481: Move to final * eip-2481: make linter happy * Walidator fixes Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * eip-2481: fix a few validator errors Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Sam Wilson --- EIPS/eip-2481.md | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/EIPS/eip-2481.md b/EIPS/eip-2481.md index 48c87b5031d3c3..c8bc379b8d6dfc 100644 --- a/EIPS/eip-2481.md +++ b/EIPS/eip-2481.md @@ -1,25 +1,21 @@ --- eip: 2481 -title: "eth/66: request identifier" +title: eth/66 request identifier +description: Introduces a request id for all requests of the eth protocol author: Christoph Burgdorf (@cburgdorf) -discussions-to: https://github.com/ethereum/EIPs/issues/2482 -status: Last Call -last-call-deadline: 2021-08-11 +discussions-to: https://ethereum-magicians.org/t/eip-2481-eth-66-request-identifiers/12132 +status: Final type: Standards Track category: Networking created: 2020-01-17 requires: 2464 --- -## Simple Summary - -This document proposes a way to increase the efficiency of the `eth` networking protocol while at the same time reducing complexity in Ethereum node implementations. It does so by introducing a request id to all requests which their corresponding responses must include. - ## Abstract -The `eth` protocol defines various request and response commands that are used to exchange data between Ethereum nodes. For example, to ask a peer node for a specific set of headers, a node sends it the [`GetBlockHeaders`](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#getblockheaders-0x03) command. +The `eth` protocol defines various request and response commands that are used to exchange data between Ethereum nodes. For example, to ask a peer node for a specific set of headers, a node sends it the [`GetBlockHeaders`](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md#getblockheaders-0x03) command. -*Citing from the [`GetBlockHeaders` spec definition](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#getblockheaders-0x03):* +*Citing from the [`GetBlockHeaders` spec definition](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md#getblockheaders-0x03):* >`[block: {P, B_32}, maxHeaders: P, skip: P, reverse: P in {0, 1}]` @@ -28,9 +24,9 @@ headers, of rising number when `reverse` is `0`, falling when `1`, `skip` blocks beginning at block `block` (denoted by either number or hash) in the canonical chain, and with at most `maxHeaders` items. -The node that receives the `GetBlockHeaders` command should answer it with the [`BlockHeaders`](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#blockheaders-0x04) response command accordingly. +The node that receives the `GetBlockHeaders` command should answer it with the [`BlockHeaders`](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md#blockheaders-0x04) response command accordingly. -*Citing from the [`BlockHeaders` spec definition](https://github.com/ethereum/devp2p/blob/master/caps/eth.md#blockheaders-0x04):* +*Citing from the [`BlockHeaders` spec definition](https://github.com/ethereum/devp2p/blob/40ab248bf7e017e83cc9812a4e048446709623e8/caps/eth.md#blockheaders-0x04):* >`[blockHeader_0, blockHeader_1, ...]` @@ -140,16 +136,7 @@ This EIP extends the `eth` protocol in a backwards incompatible way and requires This EIP does not change the consensus engine, thus does *not* require a hard fork. -## Implementation - -Trinity has a [draft PR](https://github.com/ethereum/trinity/pull/1672) with an implementation. -Geth [PR](https://github.com/ethereum/go-ethereum/pull/22241). - -## Security Considerations - -None - -## Test cases +## Test Cases These testcases cover RLP-encoding of all the redefined messages types, where the `rlp` portion is the rlp-encoding of the message defined in the `data` portion. @@ -170,6 +157,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "GetBlockHeadersPacket66", @@ -186,6 +174,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "BlockHeadersPacket66", @@ -215,6 +204,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "GetBlockBodiesPacket66", @@ -228,6 +218,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "BlockBodiesPacket66", @@ -287,6 +278,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "GetNodeDataPacket66", @@ -300,6 +292,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "NodeDataPacket66", @@ -313,6 +306,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "GetReceiptsPacket66", @@ -326,6 +320,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "ReceiptsPacket66", @@ -380,6 +375,7 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } } ``` + ```json { "type": "PooledTransactionsPacket66", @@ -416,6 +412,9 @@ These testcases cover RLP-encoding of all the redefined messages types, where th } ``` +## Security Considerations + +None ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From e40865d17e1913d14cf5676beb028c9904c82e07 Mon Sep 17 00:00:00 2001 From: Ahmad Bitar <33181301+smartprogrammer93@users.noreply.github.com> Date: Mon, 19 Dec 2022 13:35:42 +0300 Subject: [PATCH 055/274] Remove rerminating instructions validation (#6168) --- EIPS/eip-4750.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 2da4893dbd9947..9cca0550a75133 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -125,10 +125,9 @@ If the code is valid EOF1, the following execution rules apply: In addition to container format validation rules above, we extend code section validation rules (as defined in [EIP-3670](./eip-3670.md)). 1. Code validation rules of EIP-3670 are applied to every code section. -2. List of allowed *terminating instructions* in EIP-3670 is extended to include `RETF` and `JUMPF`. (*Note that `CALLF` and `JUMPF`, like other instructions with immediates, cannot be truncated.*) -3. Code section is invalid in case an immediate argument of any `CALLF` or `JUMPF` is greater than or equal to the total number of code sections. -4. Code section is invalid in case an immediate argument of any `JUMPF` is such that `type[callee_section_index].outputs != type[caller_section_index].outputs`, i.e. it is allowed to only jump to functions with the same output type. -5. `RJUMP`, `RJUMPI` and `RJUMPV` immediate argument value (jump destination relative offset) validation: +2. Code section is invalid in case an immediate argument of any `CALLF` or `JUMPF` is greater than or equal to the total number of code sections. +3. Code section is invalid in case an immediate argument of any `JUMPF` is such that `type[callee_section_index].outputs != type[caller_section_index].outputs`, i.e. it is allowed to only jump to functions with the same output type. +4. `RJUMP`, `RJUMPI` and `RJUMPV` immediate argument value (jump destination relative offset) validation: 1. Code section is invalid in case offset points to a position outside of section bounds. 2. Code section is invalid in case offset points to one of two bytes directly following `CALLF` or `JUMPF` instruction. From 684f4225c3ef3c7376cc27e41e40249eacf8d5c2 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 20 Dec 2022 00:46:52 +0100 Subject: [PATCH 056/274] Add a note about inter-contract compatibility of EIP-6059 (#6173) The note about EIP-6059 being able to support inter-operation between multiple NFT collections was added. --- EIPS/eip-6059.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6059.md b/EIPS/eip-6059.md index 4a6692e713cc0c..2a1430bf4165ae 100644 --- a/EIPS/eip-6059.md +++ b/EIPS/eip-6059.md @@ -405,7 +405,7 @@ The proposal enforces that a parent token can't be nested into one of its child 7. **How does this proposal differ from the other proposals trying to address a similar problem?** -This interface allows for tokens to both be sent to and receive other tokens. The propose-accept and parent governed patterns allow for a more secure use. The backward compatibility is only added for EIP-721, allowing for a simpler interface. +This interface allows for tokens to both be sent to and receive other tokens. The propose-accept and parent governed patterns allow for a more secure use. The backward compatibility is only added for EIP-721, allowing for a simpler interface. The proposal also allows for different collections to inter-operate, meaning that nesting is not locked to a single smart contract, but can be executed between completely separate NFT collections. ### Propose-Commit pattern for child token management From 7c8e7e6eb5f80a091998479eb57dbfea8386e8f3 Mon Sep 17 00:00:00 2001 From: William Entriken Date: Tue, 20 Dec 2022 13:17:44 -0500 Subject: [PATCH 057/274] Finalize (#6174) --- EIPS/eip-6049.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-6049.md b/EIPS/eip-6049.md index dfcfa0a549c8c8..f0159cb299c91d 100644 --- a/EIPS/eip-6049.md +++ b/EIPS/eip-6049.md @@ -4,8 +4,7 @@ title: Deprecate SELFDESTRUCT description: Deprecate SELFDESTRUCT by discouraging its use and warning about a potential future behavior change. author: William Entriken (@fulldecent) discussions-to: https://ethereum-magicians.org/t/deprecate-selfdestruct/11907 -status: Last Call -last-call-deadline: 2022-12-20 +status: Final type: Meta created: 2022-11-27 --- From 694fbebc12a67c7dc5ac2d8ac1be6106808c29b0 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 13:33:49 -0500 Subject: [PATCH 058/274] Update EIP-5615: Change totalSupply behavior when exists is false (#6176) --- EIPS/eip-5615.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5615.md b/EIPS/eip-5615.md index 988c8fbbd6e75f..ec4900adc4f4c5 100644 --- a/EIPS/eip-5615.md +++ b/EIPS/eip-5615.md @@ -26,8 +26,7 @@ interface ERC1155Supply is ERC1155 { // @return Whether the given token id exists, previously existed, or may exist function exists(uint256 id) external view returns (bool); - // @notice If the given id exists, previously existed, or may exist, this function MUST return the number of tokens with a given id. Otherwise, this function MUST revert. - // @dev This MUST revert if exists(id) returns false + // @notice This function MUST return the number of tokens with a given id. If the token id does not exist, it MUST return 0. // @param id The token id of which fetch the total supply // @return The total supply of the given token id function totalSupply(uint256 id) external view returns (uint256); @@ -40,6 +39,10 @@ This EIP does not implement [EIP-165](./eip-165.md), as this interface is simple The `totalSupply` and `exists` functions were modeled after [EIP-721](./eip-721.md) and [EIP-20](./eip-20.md). +`totalSupply` does not revert if the token ID does not exist, since contracts that care about that case should use `exists` instead (which might return false even if `totalSupply` is zero). + +`exists` is included to differentiate between the two ways that `totalSupply` could equal zero (either no tokens with the given ID have been minted yet, or no tokens with the given ID will ever be minted). + ## Backwards Compatibility This EIP is designed to be backward compatible with the OpenZeppelin `ERC1155Supply`. From 607e39bb3ccaa7b0fc4bc7b45add21fe49fa2a18 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:54:31 -0500 Subject: [PATCH 059/274] Update EIP-1: Use updated list of EL clients (#6161) --- EIPS/eip-1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-1.md b/EIPS/eip-1.md index f5a96deb32280e..e9812ec609df91 100644 --- a/EIPS/eip-1.md +++ b/EIPS/eip-1.md @@ -100,7 +100,7 @@ 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. -- 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). +- 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 (besu, erigon, ethereumjs, go-ethereum, nethermind, or others). - 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. From 5b3d18cf9a0037b6481fe79559dd097b6955b17c Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:59:19 -0500 Subject: [PATCH 060/274] CI: Add Link Checker (#5361) * Replace HTMLProofer with a faster alternative * Add markdown link checker config * What a weird bug * Try this fix? * Try recursively `chown`ing the directory? * Please let this work :| * Escape the star * Please work... * Update ci.yml * Move to config directory * Oops --- .github/workflows/ci.yml | 27 ++++++++++++++++++++------- config/mlc_config.json | 13 +++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 config/mlc_config.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7535193a45372..0cd5b8c17fa566 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,31 +39,44 @@ jobs: htmlproofer: name: HTMLProofer runs-on: ubuntu-20.04 - + 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 - - name: Install Ruby uses: ruby/setup-ruby@08245253a76fa4d1e459b7809579c62bd9eb718a with: ruby-version: 2.6.0 bundler-cache: true - + - name: Build Website run: | bundle exec jekyll doctor bundle exec jekyll build - + - name: HTML Proofer run: bundle exec htmlproofer ./_site --check-html --check-opengraph --report-missing-names --log-level=:debug --assume-extension --empty-alt-ignore --timeframe=6w --disable-external - + - name: DNS Validator run: bundle exec github-pages health-check + link-check: + name: Link Check + runs-on: ubuntu-latest + + steps: + - name: Checkout EIP Repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + + - name: Link Checker + uses: gaurav-nelson/github-action-markdown-link-check@0a51127e9955b855a9bbfa1ff5577f1d1338c9a5 + with: + config-file: config/mlc_config.json + use-quiet-mode: no + use-verbose-mode: yes + check-modified-files-only: yes + codespell: name: CodeSpell runs-on: ubuntu-latest diff --git a/config/mlc_config.json b/config/mlc_config.json new file mode 100644 index 00000000000000..c3872447078e07 --- /dev/null +++ b/config/mlc_config.json @@ -0,0 +1,13 @@ +{ + "ignorePatterns": [], + "replacementPatterns": [ + { + "pattern": "^/", + "replacement": "/github/workspace/" + } + ], + "aliveStatusCodes": [ + 200, + 429 + ] +} From d4a445968e8ff51f3b685b3f807511b5ef680fa0 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 14:59:56 -0500 Subject: [PATCH 061/274] Update EIP-5289: Add EIP-5568 signals, other extensibility, and security considerations (#6179) * Update EIP-5289: Add EIP-5568 signals, other extensibility, and security considerations * Add example popup * Remove old parameter * Update solidity interface * Update reference implementation with changes * Use correct EIP # --- EIPS/eip-5289.md | 100 ++++++------------ assets/eip-5289/ERC5289Library.sol | 14 ++- assets/eip-5289/example-popup.png | Bin 0 -> 41841 bytes .../eip-5289/interfaces/IERC5289Library.sol | 8 +- 4 files changed, 44 insertions(+), 78 deletions(-) create mode 100644 assets/eip-5289/example-popup.png diff --git a/EIPS/eip-5289.md b/EIPS/eip-5289.md index 616989d686c5a9..cb08bd694275b5 100644 --- a/EIPS/eip-5289.md +++ b/EIPS/eip-5289.md @@ -8,7 +8,7 @@ status: Draft type: Standards Track category: ERC created: 2022-07-16 -requires: 165 +requires: 165, 5568 --- ## Abstract @@ -25,85 +25,53 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ### Legal Contract Library Interface -See [`IERC5289Library`](../assets/eip-5289/interfaces/IERC5289Library.sol). - -### Requesting a Signature - -To request that certain documents be signed, revert with the following reason: - ```solidity -string.concat("5289:", libraryAddress1, "-", documentId1OfAddress1, "-", documentId2OfAddress1 ",", libraryAddress2, "-", documentId1OfAddress2, ...) +/// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +interface IERC5289Library is IERC165 { + /// @notice Emitted when signDocument is called + event DocumentSigned(address indexed signer, uint16 indexed documentId); + + /// @notice An immutable link to the legal document (RECOMMENDED to be hosted on IPFS). This MUST use a common file format, such as PDF, HTML, TeX, or Markdown. + function legalDocument(uint16 documentId) external view returns (string memory); + + /// @notice Returns whether or not the given user signed the document. + function documentSigned(address user, uint16 documentId) external view returns (bool signed); + + /// @notice Returns when the the given user signed the document. + /// @dev If the user has not signed the document, the timestamp may be anything. + function documentSignedAt(address user, uint16 documentId) external view returns (uint64 timestamp); + + /// @notice Sign a document + /// @dev This MUST be validated by the smart contract. This MUST emit DocumentSigned or throw. + function signDocument(address signer, uint16 documentId) external; +} ``` -NOTE: If an address begins with one or more zeros, they may be omitted. Addresses are represented in base 64. -NOTE 2: The document IDs are represented in base 64. +### Requesting a Signature -Example: +To request that certain documents be signed, revert with an [EIP-5568](./eip-5568.md) signal. The format of the `instruction_data` is an ABI-encoded `(address, uint16)` pair, where the address is the address of the library, and the `uint16` is the `documentId` of the document: ```solidity -"5289:1-1-2,hElLO/0-7b+A" -``` - -#### Base 64 - -From RFC 4648: - -```text - Table 1: The Base 64 Alphabet - - Value Encoding Value Encoding Value Encoding Value Encoding - 0 A 17 R 34 i 51 z - 1 B 18 S 35 j 52 0 - 2 C 19 T 36 k 53 1 - 3 D 20 U 37 l 54 2 - 4 E 21 V 38 m 55 3 - 5 F 22 W 39 n 56 4 - 6 G 23 X 40 o 57 5 - 7 H 24 Y 41 p 58 6 - 8 I 25 Z 42 q 59 7 - 9 J 26 a 43 r 60 8 - 10 K 27 b 44 s 61 9 - 11 L 28 c 45 t 62 + - 12 M 29 d 46 u 63 / - 13 N 30 e 47 v - 14 O 31 f 48 w (pad) = - 15 P 32 g 49 x - 16 Q 33 h 50 y +throw WalletSignal24(0, 5289, abi.encode(0xcbd99eb81b2d8ca256bb6a5b0ef7db86489778a7, 12345)); ``` -NOTE: Padding is NOT used. - -Here is a TypeScript snippet to convert `Number` objects into base 64 strings and back. - -```typescript -const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/"; - -function toB64(x: Number): string { - return x - .toString(2) - .split(/(?=(?:.{6})+(?!.))/g) - .map(bin => parseInt(bin, 2)) - .map(digit => digits[digit]) - .join(""); -} +### Signing a Document -function fromB64(x: string): Number { - return Number.parseInt( - x.split("").reduce((bin, digit) => bin + new Number(digits.indexOf(digit)).toString(2).padStart(6, '0'), ""), - 2 - ); -} -``` +When a signature is requested, wallets MUST call `legalDocument`, display the resulting document to the user, and prompt them to either sign the document or cancel: -### Signing a Document +![image](../assets/eip-5289/example-popup.png) -When a signature is requested, wallets MUST call `legalDocument` and fetch the file off of IPFS, and render that file to the user. If the user agrees, the wallet MUST call `signDocument`. Using a form of account abstraction is RECOMMENDED. +If the user agrees, the wallet MUST call `signDocument`. ## Rationale - `uint64` was chosen for the timestamp return type as 64-bit time registers are standard. - `uint16` was chosen for the document ID as 65536 documents are likely sufficient for any use case, and the contract can always be re-deployed. -- `signDocument` used to take a signature, but due to account abstraction being imminent, this was deemed unnecessary. +- `signDocument` doesn't take an ECDSA signature for future compatibility with account abstraction. In addition, future extensions can supply this functionality. - IPFS is mandatory because the authenticity of the signed document can be proven. ## Backwards Compatibility @@ -114,11 +82,11 @@ No backwards compatibility issues found. ### Legal Contract Library -See [`ERC5289Library`](../assets/eip-5289/ERC5289Library.sol). +See [`IERC5289Library`](../assets/eip-5289/interfaces/IERC5289Library.sol), [`ERC5289Library`](../assets/eip-5289/ERC5289Library.sol). ## Security Considerations -No security considerations found. +Users can claim that their private key was stolen and used to fraudulently "sign" contracts. As such, **documents must only be permissive in nature, not restrictive.** For example, a document granting a license to use the image attached to an NFT would be acceptable, as there is no reason for the signer to plausibly deny signing the document. ## Copyright diff --git a/assets/eip-5289/ERC5289Library.sol b/assets/eip-5289/ERC5289Library.sol index d6b7867fe1281d..04bb6c72d8ffbb 100644 --- a/assets/eip-5289/ERC5289Library.sol +++ b/assets/eip-5289/ERC5289Library.sol @@ -6,19 +6,18 @@ import "./interfaces/IERC5289Library.sol"; contract ERC5289Library is IERC165, IERC5289Library { uint16 private counter = 0; - mapping(uint16 => bytes) private multihashes; + mapping(uint16 => string) private uris; mapping(uint16 => mapping(address => uint64)) signedAt; - mapping(uint16 => mapping(address => bytes)) signatures; constructor() { } - function registerDocument(bytes memory multihash) public returns (uint16) { - multihashes[counter] = multihash; + function registerDocument(string memory uri) public returns (uint16) { + uris[counter] = uri; return counter++; } - function legalDocument(uint16 documentId) public view returns (bytes memory) { - return multihashes[documentId]; + function legalDocument(uint16 documentId) public view returns (string uri) { + return uris[documentId]; } function documentSigned(address user, uint16 documentId) public view returns (bool isSigned) { @@ -29,11 +28,10 @@ contract ERC5289Library is IERC165, IERC5289Library { return signedAt[documentId][user]; } - function signDocument(address signer, uint16 documentId, bytes memory signature) public { + function signDocument(address signer, uint16 documentId) public { require(signer == msg.sender, "invalid user"); signedAt[documentId][msg.sender] = uint64(block.timestamp); - signatures[documentId][msg.sender] = signature; emit DocumentSigned(msg.sender, documentId); } diff --git a/assets/eip-5289/example-popup.png b/assets/eip-5289/example-popup.png new file mode 100644 index 0000000000000000000000000000000000000000..957bb6ba315232b1094103b43345729967c94709 GIT binary patch literal 41841 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 kkm!QC*ZY{W|G%04_h%D(nRvZ%>V!Z literal 0 HcmV?d00001 diff --git a/assets/eip-5289/interfaces/IERC5289Library.sol b/assets/eip-5289/interfaces/IERC5289Library.sol index 014d156da97099..6eb68b152beb9a 100644 --- a/assets/eip-5289/interfaces/IERC5289Library.sol +++ b/assets/eip-5289/interfaces/IERC5289Library.sol @@ -7,8 +7,8 @@ interface IERC5289Library is IERC165 { /// @notice Emitted when signDocument is called event DocumentSigned(address indexed signer, uint16 indexed documentId); - /// @notice The IPFS multihash of the legal document. This MUST use a common file format, such as PDF, HTML, TeX, or Markdown. - function legalDocument(uint16 documentId) external view returns (bytes memory); + /// @notice An immutable link to the legal document (RECOMMENDED to be hosted on IPFS). This MUST use a common file format, such as PDF, HTML, TeX, or Markdown. + function legalDocument(uint16 documentId) external view returns (string memory); /// @notice Returns whether or not the given user signed the document. function documentSigned(address user, uint16 documentId) external view returns (bool signed); @@ -17,7 +17,7 @@ interface IERC5289Library is IERC165 { /// @dev If the user has not signed the document, the timestamp may be anything. function documentSignedAt(address user, uint16 documentId) external view returns (uint64 timestamp); - /// @notice Provide a signature + /// @notice Sign a document /// @dev This MUST be validated by the smart contract. This MUST emit DocumentSigned or throw. - function signDocument(address signer, uint16 documentId, bytes memory signature) external; + function signDocument(address signer, uint16 documentId) external; } From e533e3920d4f155bfd96ae19384ade23b3f5eb63 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 15:14:14 -0500 Subject: [PATCH 062/274] Update EIP-5507: Add Rationale stub (#6181) --- EIPS/eip-5507.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index 06743c467e8797..aacca4f0a7ecad 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -143,7 +143,7 @@ interface IERC1155Refund is IERC1155, IERC165 { ## Rationale -TODO +`refundDeadlineOf` uses blocks instead of timestamps, as timestamps are less reliable than block numbers. ## Backwards Compatibility From b6a0fc8bd8a474aaf0dbb4e82d062dba870bcf8e Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 15:15:19 -0500 Subject: [PATCH 063/274] Update EIP-5219: Fix small typo (#6182) --- EIPS/eip-5219.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-5219.md b/EIPS/eip-5219.md index fa1d8f14b7e097..47036c23a98267 100644 --- a/EIPS/eip-5219.md +++ b/EIPS/eip-5219.md @@ -49,6 +49,7 @@ The `request` method was chosen to be readonly because all data should be sent t - Other EIPs can be used to request state changing operations in conjunction with a `307 Temporary Redirect` status code. Instead of mimicking a full HTTP request, a highly slimmed version was chosen for the following reasons: + - The only particularly relevant HTTP method is `GET` - Query parameters can be encoded in the resource. - Request headers are, for the most part, unnecessary for `GET` requests. From 9a2340635de585fee046517aec091e489174ea6c Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 15:35:19 -0500 Subject: [PATCH 064/274] Update EIP-5380: First update (#6184) --- EIPS/eip-5380.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-5380.md b/EIPS/eip-5380.md index d86b7ada2b5c03..f4006789463342 100644 --- a/EIPS/eip-5380.md +++ b/EIPS/eip-5380.md @@ -13,7 +13,7 @@ requires: 165, 721 ## Abstract -This EIP proposes extensions of the commonly-used [EIP-721](./eip-721.md) standard that allows token owners to grant limited usage of those tokens to other addresses. +This EIP proposes a new interface that allows [EIP-721](./eip-721.md) token owners to grant limited usage of those tokens to other addresses. ## Motivation @@ -23,13 +23,13 @@ There are many scenarios in which it makes sense for the owner of a token to gra 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. -### `IERC5380` +### Base ```solidity /// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; -interface ERC5380 is ERC165 { +interface ERC721Entitlement is ERC165 { /// @notice Emitted when the amount of entitlement a user has changes. If user is the zero address, then the user is the owner event EntitlementChanged(address indexed user, address indexed contract, uint256 indexed tokenId); @@ -53,9 +53,9 @@ interface ERC5380 is ERC165 { } ``` -`supportsInterface` MUST return true when called with `IERC5380`'s interface ID. +`supportsInterface` MUST return true when called with `ERC721Entitlement`'s interface ID. -### `IERC5380Enumerable` +### Enumerable This OPTIONAL interface is RECOMMENDED. @@ -63,7 +63,7 @@ This OPTIONAL interface is RECOMMENDED. /// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; -interface ERC5380Enumerable is ERC5380 /* , ERC165 */ { +interface ERC721EntitlementEnumerable is ERC721Entitlement /* , ERC165 */ { /// @notice Enumerate tokens assigned to a user /// @dev Throws if the index is out of bounds or if user == address(0) /// @param user The user to query @@ -72,11 +72,11 @@ interface ERC5380Enumerable is ERC5380 /* , ERC165 */ { } ``` -`supportsInterface` MUST return true when called with `IERC5380Enumerable`'s interface ID. +`supportsInterface` MUST return true when called with `ERC721EntitlementEnumerable`'s interface ID. ## Rationale -TODO. +[EIP-20](./eip-20.md) and [EIP-1155](./eip-1155.md) are unsupported as partial ownership is much more complex to track than boolean ownership. ## Backwards Compatibility From 047f16d7fd9f805f3177610161259aca90844dd3 Mon Sep 17 00:00:00 2001 From: Mikhail Kalinin Date: Wed, 21 Dec 2022 03:53:33 +0600 Subject: [PATCH 065/274] Add EIP-6110: Supply validator deposits on chain (#6110) * EIP-6110: initial draft * Fix header * Fill out the discussions-to * Make linter happy * Fix color schema for python snippets * Apply suggestions from code review Co-authored-by: Danny Ryan * Fix anchor * Fix eip4881 link * Fix eip4881 link once again Co-authored-by: Danny Ryan --- EIPS/eip-6110.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 EIPS/eip-6110.md diff --git a/EIPS/eip-6110.md b/EIPS/eip-6110.md new file mode 100644 index 00000000000000..64dfa7a9302234 --- /dev/null +++ b/EIPS/eip-6110.md @@ -0,0 +1,192 @@ +--- +eip: 6110 +title: Supply validator deposits on chain +description: Provides validator deposits as a list of deposit operations added to the Execution Layer block +author: Mikhail Kalinin (@mkalinin), Danny Ryan (@djrtwo) +discussions-to: https://ethereum-magicians.org/t/eip-6110-supply-validator-deposits-on-chain/12072 +status: Draft +type: Standards Track +category: Core +created: 2022-12-09 +--- + +## Abstract + +Appends validator deposits to the Execution Layer block structure. This shifts responsibliity of deposit inclusion and validation to the Execution Layer and removes the need for deposit (or `eth1data`) voting from the Consensus Layer. + +Validator deposits list supplied in a block is obtained by parsing deposit contract log events emitted by each deposit transaction included in a given block. + +## Motivation + +Validator deposits are a core component of the proof-of-stake consensus mechanism. This EIP allows for an in-protocol mechanism of deposit processing on the Consensus Layer and eliminates the proposer voting mechanism utilized currently. This proposed mechanism relaxes safety assumptions and reduces complexity of client software design, contributing to the security of the deposits flow. It also improves validator UX. + +Advantages of in-protocol deposit processing consist of but are not limit to the following: + +* Significant increase of deposits security by supplanting proposer voting. With the proposed in-protocol mechanism, an honest online node can't be convinced to process fake deposits even when more than 2/3 portion of stake is adversarial. +* Decrease of delay between submitting deposit transaction on Execution Layer and its processing on Consensus Layer. That is, ~13 minutes with in-protocol deposit processing compared to ~12 hours with the existing mechanism. +* Eliminate beacon block proposal dependency on JSON-RPC API data polling that suffers from failures caused by inconsistencies between JSON-RPC API implementations and dependency of API calls processing on the inner state (e.g. syncing) of client software. +* Eliminate requirement to maintain and distribute deposit contract snapshots ([EIP-4881](./eip-4881.md)). +* Reduction of design and engineering complexity of Consensus Layer client software on a component that has proven to be brittle. + +## Specification + +### Constants + +| Name | Value | Comment | +| - | - | - | +|`FORK_TIMESTAMP` | *TBD* | Mainnet | + +### Configuration + +| Name | Value | Comment | +| - | - | - | +|`DEPOSIT_CONTRACT_ADDRESS` | `0x00000000219ab540356cbb839cbe05303d7705fa` | Mainnet | + +`DEPOSIT_CONTRACT_ADDRESS` parameter **MUST** be included into client software binary distribution. + +### Definitions + +* **`FORK_BLOCK`** -- the first block in a blockchain with the `timestamp` greater or equal to `FORK_TIMESTAMP`. + +### Deposit + +The structure denoting the new deposit operation consists of the following fields: + +1. `pubkey: Bytes48` +2. `withdrawal_credentials: Bytes32` +3. `amount: uint64` +4. `signature: Bytes96` +5. `index: uint64` + +RLP encoding of deposit structure **MUST** be computed in the following way: + +```python +rlp_encoded_deposit = RLP([ + RLP(pubkey), + RLP(withdrawal_credentials), + RLP(amount), + RLP(signature), + RLP(index) +]) +``` + +### Block structure + +Beginning with the `FORK_BLOCK`, the block body **MUST** be appended with a list of deposit operations. RLP encoding of an extended block body structure **MUST** be computed as follows: + +```python +block_body_rlp = RLP([ + rlp_encoded_field_0, + ..., + # Latest block body field before `deposits` + rlp_encoded_field_n, + + RLP([rlp_encoded_deposit_0, ..., rlp_encoded_deposit_k]) +]) +``` + +Beginning with the `FORK_BLOCK`, the block header **MUST** be appended with the new **`deposits_root`** field. The value of this field is the trie root committing to the list of deposits in the block body. **`deposits_root`** field value **MUST** be computed as follows: + +```python +def compute_trie_root_from_indexed_data(data): + trie = Trie.from([(i, obj) for i, obj in enumerate(data)]) + return trie.root + +block.header.deposits_root = compute_trie_root_from_indexed_data(block.body.deposits) +``` + +### Block validity + +Beginning with the `FORK_BLOCK`, client software **MUST** extend block validity rule set with the following conditions: + +1. Value of **`deposits_root`** block header field equals to the trie root committing to the list of deposit operations contained in the block. To illustrate: + +```python +def compute_trie_root_from_indexed_data(data): + trie = Trie.from([(i, obj) for i, obj in enumerate(data)]) + return trie.root + +assert block.header.deposits_root == compute_trie_root_from_indexed_data(block.body.deposits) +``` + +2. The list of deposit operations contained in the block **MUST** be equivalent to the list of log events emitted by each deposit transaction of the given block, respecting the order of transaction inclusion. To illustrate: + +```python +def parse_deposit_data(deposit_event_data): bytes[] + """ + Parses Deposit operation from DepositContract.DepositEvent data + """ + pass + +def little_endian_to_uint64(data: bytes): uint64 + return uint64(int.from_bytes(data, 'little')) + +class Deposit(object): + pubkey: Bytes48 + withdrawal_credentials: Bytes32 + amount: uint64 + signature: Bytes96 + index: uint64 + +def event_data_to_deposit(deposit_event_data): Deposit + deposit_data = parse_deposit_data(deposit_event_data) + return Deposit( + pubkey=Bytes48(deposit_data[0]), + withdrawal_credentials=Bytes32(deposit_data[1]), + amount=little_endian_to_uint64(deposit_data[2]), + signature=Bytes96(deposit_data[3]), + index=little_endian_to_uint64(deposit_data[4]) + ) + +# Obtain receipts from block execution result +receipts = block.execution_result.receipts + +# Retrieve all deposits made in the block +expected_deposits = [] +for receipt in receipts: + for log in receipt.logs: + if log.address == DEPOSIT_CONTRACT_ADDRESS: + deposit = event_data_to_deposit(log.data) + expected_deposits.append(deposit) + +# Compare retrieved deposits to the list in the block body +assert block.body.deposits == expected_deposits +``` + +A block that does not satisfy the above conditions **MUST** be deemed invalid. + +## Rationale + +### `index` field + +The `index` field may appear unnecessary but it is important information for deposit processing flow on the Consensus Layer. + +### Not limiting the size of deposit operations list + +The list is unbounded because of negligible data complexity and absence of potential DoS vectors. See [Security Considerations](#security-considerations) for more details. + +### Filtering events only by `DEPOSIT_CONTRACT_ADDRESS` + +Deposit contract does not emit any events except for `DepositEvent`, thus additional filtering is unnecessary. + +## Backwards Compatibility + +This EIP introduces backwards incompatible changes to the block structure and block validation rule set. But neither of these changes break anything related to user activity and experience. + +## Security Considerations + +### Data complexity + +At the time of writing this document, the total number of submitted deposits is 478,402 which is 88MB of deposit data. Assuming frequency of deposit transactions remains the same, historic chain data complexity induced by this EIP can be estimated as 50MB per year which is negligible in comparison to other historic data. + +After the beacon chain launch in December 2020, the biggest observed spike in a number of submitted deposits was on March 15, 2022. More than 6000 deposit transactions were submitted during 24 hours which on average is less than 1 deposit, or 192 bytes of data, per block. + +Considering the above, we conclude that data complexity introduced by this proposal is negligible. + +### DoS vectors + +With 1 ETH as a minimum deposit amount, the lowest cost of a byte of deposit data is 1 ETH/192 ~ 5,208,333 Gwei. This is several orders of magnitude higher than the cost of a byte of transaction's calldata, thus adding deposit operations to a block does not increase Execution Layer DoS attack surface. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 220fbe24eb7677ce77372ad946797e7ac695b72f Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 20 Dec 2022 20:02:31 -0500 Subject: [PATCH 066/274] Update EIP-5507: Fix typo (wei is a reserved name) (#6191) --- EIPS/eip-5507.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index aacca4f0a7ecad..e91fc9180836c7 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -51,8 +51,8 @@ interface IERC20Refund is ERC20, ERC165 { function refund(uint256 amount) external; /// @notice Gets the refund price - /// @return wei The amount of ether (in wei) that would be refunded for a single token unit (10**decimals smallest divisible units) - function refundOf() external view returns (uint256 wei); + /// @return _wei The amount of ether (in wei) that would be refunded for a single token unit (10**decimals smallest divisible units) + function refundOf() external view returns (uint256 _wei); /// @notice Gets the first block for which the refund is not active /// @return block The block beyond which the token cannot be refunded @@ -89,8 +89,8 @@ interface IERC721Refund is ERC721, ERC165 { /// @notice Gets the refund price of the specific `tokenId` /// @param tokenId The `tokenId` to query - /// @return wei The amount of ether (in wei) that would be refunded - function refundOf(uint256 tokenId) external view returns (uint256 wei); + /// @return _wei The amount of ether (in wei) that would be refunded + function refundOf(uint256 tokenId) external view returns (uint256 _wei); /// @notice Gets the first block for which the refund is not active for a given `tokenId` /// @param tokenId The `tokenId` to query @@ -131,8 +131,8 @@ interface IERC1155Refund is IERC1155, IERC165 { /// @notice Gets the refund price of the specific `tokenId` /// @param tokenId The `tokenId` to query - /// @return wei The amount of ether (in wei) that would be refunded for a single token - function refundOf(uint256 tokenId) external view returns (uint256 wei); + /// @return _wei The amount of ether (in wei) that would be refunded for a single token + function refundOf(uint256 tokenId) external view returns (uint256 _wei); /// @notice Gets the first block for which the refund is not active for a given `tokenId` /// @param tokenId The `tokenId` to query From f29a4248a93e1f89839023b790e20ad39b426aaf Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Wed, 21 Dec 2022 14:52:29 -0700 Subject: [PATCH 067/274] 3540,4570,5450: update EOF container format (#6156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 3540: update eof contain format * 4750,5450: update for new 3540 changes, disallow unreachble code * 4750,5450: remove ref impls for now * appease our overlord, walidator * Apply suggestions from code review Co-authored-by: Andrei Maiboroda Co-authored-by: Jochem Brouwer * reword type section intro Co-authored-by: Paweł Bylica * apply feedback from review Co-authored-by: Paweł Bylica * 4750: do not cleanup stack after jumpf or retf * remove rationale for stack cleanup * Apply suggestions from code review Co-authored-by: Paweł Bylica * 4750: remove jumpf for later definition * fix stale comment Co-authored-by: Danno Ferrin * fix a couple typos and improve clarity * appease link checker * note terminating instruction requirement from 5450 * cap max_stack_height at 1023 * add padding to table * fix padding Co-authored-by: Andrei Maiboroda * revert terminating instructions change to 3670 * fix rearranged sections Co-authored-by: Andrei Maiboroda * Apply suggestions from code review Co-authored-by: Paweł Bylica Co-authored-by: Andrei Maiboroda Co-authored-by: Jochem Brouwer Co-authored-by: Paweł Bylica Co-authored-by: Danno Ferrin --- EIPS/eip-3540.md | 215 +++++++++++++--------------------------- EIPS/eip-3670.md | 1 - EIPS/eip-4750.md | 251 ++++------------------------------------------- EIPS/eip-5450.md | 126 ++---------------------- 4 files changed, 95 insertions(+), 498 deletions(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 6ac7732035c7a1..05e40a0dd026bd 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -8,7 +8,7 @@ status: Review type: Standards Track category: Core created: 2021-03-16 -requires: 3541, 3860 +requires: 3541, 3860, 4750, 5450 --- ## Abstract @@ -52,6 +52,8 @@ The *initcode* is the code executed in the context of the *create* transaction, The opcode `0xEF` is currently an undefined instruction, therefore: *It pops no stack items and pushes no stack items, and it causes an exceptional abort when executed.* This means *initcode* or already deployed *code* starting with this instruction will continue to abort execution. +Unless otherwised specified, all integers are encoded in big-endian byte order. + ### Code validation We introduce *code validation* for new contract creation. To achieve this, we define a format called EVM Object Format (EOF), containing a version indicator, and a ruleset of validity tied to a given version. @@ -79,43 +81,76 @@ The container starts with the EOF header: | magic | 2-bytes | 0xEF00 | | | version | 1-byte | 0x01–0xFF | EOF version number | -The EOF header is followed by at least one section header. Each section header contains two fields, `section_kind` and `section_size`. +The EOF header is followed by at least one section header. Each section header contains two fields, `section_kind` and either `section_size` or `section_size_list`, depending on the kind. `section_size_list` is a list of size values when multiple sections of this kind are allowed. -| description | length | value | | -|--------------|---------|---------------|-------------------------------------------------| -| section_kind | 1-byte | 0x01–0xFF | Encoded as a 8-bit unsigned number. | -| section_size | 2-bytes | 0x0001–0xFFFF | Encoded as a 16-bit unsigned big-endian number. | +| description | length | value | | +|-------------------|---------|---------------|-------------------| +| section_kind | 1-byte | 0x01–0xFF | `uint8` | +| section_size | 2-bytes | 0x0001–0xFFFF | `uint16` | +| section_size_list | dynamic | n/a | `uint16, uint16+` | -The list of section headers is terminated with the *section headers terminator byte* `0x00`. +The list of section headers is terminated with the *section headers terminator byte* `0x00`. The body content follows immediately after. #### Container validation rules -1. `version` MUST NOT be `0`.[^1](#EOF-version-range-start-with-1) +1. `version` MUST NOT be `0`.[^1](#eof-version-range-start-with-1) 2. `section_kind` MUST NOT be `0`. The value `0` is reserved for *section headers terminator byte*. -3. `section_size` MUST NOT be `0`. If a section is empty its section header MUST be omitted. -4. There MUST be at least one section (and therefore section header). -5. Section data size MUST be equal to `section_size` declared in its header. -6. Stray bytes outside of sections MUST NOT be present. This includes trailing bytes after the last section. +3. There MUST be at least one section (and therefore section header). +4. Section content size MUST be equal to size declared in its header. +5. Stray bytes outside of sections MUST NOT be present. This includes trailing bytes after the last section. ### EOF version 1 -#### Section kinds +EOF version 1 is made up of 5 EIPs, including this one: [EIP-3540](./eip-3540.md), [EIP-3670](./eip-3670.md), [EIP-4200](./eip-4200.md), [EIP-4750](./eip-4750.md), and [EIP-5450](./eip-5450.md). Some values in this specification are only discussed briefly. To understand the full scope of EOF, it is necessary to review each EIP in-depth. + +#### Container + +The EOF version 1 container consists of a `header` and `body`. + +``` +container := header, body +header := magic, version, kind_type, type_size, kind_code, num_code_sections, code_size+, kind_data, data_size, terminator +body := type_section, code_section+, data_section +type_section := (inputs, outputs, max_stack_height)+ +``` + +*note: `,` is a concatenation operator and `+` should be interpreted as "one or more" of the preceding item* + +##### Header -The section kinds for EOF version 1 are defined as follows. The list may be extended in future versions. +| name | length | value | description | +|-------------------|----------|---------------|------------------------------------------------------------------------------------| +| magic | 2 bytes | 0xEF00 | EOF prefix | +| version | 1 byte | 0x01 | EOF version | +| kind_type | 1 byte | 0x01 | kind marker for EIP-4750 type section header | +| type_size | 2 bytes | 0x0003-0xFFFF | uint16 denoting the length of the type section content | +| kind_code | 1 byte | 0x02 | kind marker for code size section | +| num_code_sections | 2 bytes | 0x0001-0xFFFF | uint16 denoting the number of the code sections | +| code_size | 2 bytes | 0x0001-0xFFFF | uint16 denoting the length of the code section content | +| kind_data | 1 byte | 0x03 | kind marker for data size section | +| data_size | 2 bytes | 0x0000-0xFFFF | uint16 integer denoting the length of the data section content | +| terminator | 1 byte | 0x00 | marks the end of the header | -| section_kind | meaning | -|--------------|------------------------------------------------| -| 0 | *reserved for section headers terminator byte* | -| 1 | code | -| 2 | data | +##### Body + +| name | length | value | description | +|------------------|----------|--------------|-------------| +| type_section | variable | n/a | stores EIP-4750 and EIP-5450 code section metadata | +| inputs | 1 byte | 0x00-0x7F | number of stack elements the code section consumes | +| outputs | 1 byte | 0x00-0x7F | number of stack elements the code section returns | +| max_stack_height | 2 bytes | 0x0000-0x3FF | max height of data stack during execution | +| code_section | variable | n/a | arbitrary bytecode | +| data_section | variable | n/a | arbitrary sequence of bytes | + +See [EIP-4750](./eip-4750.md) for more information on the type section content. #### EOF version 1 validation rules 1. In addition to general validation rules above, EOF version 1 bytecode conforms to the rules specified below: - - Exactly one code section MUST be present. - - The code section MUST be the first section. - - A single data section MAY follow the code section. -2. Any other version is invalid. + - Exactly one type section header MUST be present immediately following the EOF version. Each code section MUST have a specified type signature in the type body. + - Exactly one code section header MUST be present immediately following the type section. A maxmimum of 1024 individual code sections are allowed. + - Exactly one data section header MUST be present immediately following the code section. +2. Any version other than `0x01` is invalid. (*Remark:* Contract creation code SHOULD set the section size of the data section so that the constructor arguments fit it.) @@ -123,14 +158,14 @@ The section kinds for EOF version 1 are defined as follows. The list may be exte For clarity, the *container* refers to the complete account code, while *code* refers to the contents of the code section only. -1. `JUMPDEST`-analysis is only run on the *code*. -2. Execution starts at the first byte of the *code*, and `PC` is set to 0. -3. Execution stops if `PC` goes outside the code section bounds. -4. `PC` returns the current position within the *code*. -5. `JUMP`/`JUMPI` uses an absolute offset within the *code*. -6. `CODECOPY`/`CODESIZE`/`EXTCODECOPY`/`EXTCODESIZE`/`EXTCODEHASH` keeps operating on the entire *container*. -7. The input to `CREATE`/`CREATE2` is still the entire *container*. -8. The size limit for deployed code as specified in [EIP-170](./eip-170.md) and for init code as specified in [EIP-3860](./eip-3860.md) is applied to the entire *container* size, not to the *code* size. +1. Execution starts at the first byte of the first code section, and PC is set to 0. +2. Execution stops if `PC` goes outside the code section bounds. +3. `PC` returns the current position within the *code*. +4. `CODECOPY`/`CODESIZE`/`EXTCODECOPY`/`EXTCODESIZE`/`EXTCODEHASH` keeps operating on the entire *container*. +5. The input to `CREATE`/`CREATE2` is still the entire *container*. +6. The size limit for deployed code as specified in [EIP-170](./eip-170.md) and for init code as specified in [EIP-3860](./eip-3860.md) is applied to the entire *container* size, not to the *code* size. + +(*Remark:* Due to [EIP-4750](./eip-4750.md), `JUMP` and `JUMPI` are disabled and therefore are not discussed in relation to EOF.) ### Changes to contract creation semantics @@ -198,7 +233,7 @@ Also, implementations may use APIs where 0 version number denotes legacy code. We have considered different questions for the sections: - Streaming headers (i.e. `section_header, section_data, section_header, section_data, ...`) are used in some other formats (such as WebAssembly). They are handy for formats which are subject to editing (adding/removing sections). That is not a useful feature for EVM. One minor benefit applicable to our case is that they do not require a specific "header terminator". On the other hand they seem to play worse with code chunking / merkleization, as it is better to have all section headers in a single chunk. -- Whether to have a header terminator or to encode `number_of_sections` or `total_size_of_headers`. Both raise the question of how large of a value these fields should be able to hold. While today there will be only two sections, in case each "EVM function" would become a separate code section, a fixed 8-bit field may not be big enough. A terminator byte seems to avoid these problems. +- Whether to have a header terminator or to encode `number_of_sections` or `total_size_of_headers`. Both raise the question of how large of a value these fields should be able to hold. A terminator byte seems to avoid the problem of choosing a size which is too small without any perceptible downside, so it is the path taken. - Whether to encode `section_size` as a fixed 16-bit value or some kind of variable length field (e.g. LEB128). We have opted for fixed size, because it simplifies client implementations, and 16-bit seems enough, because of the currently exposed code size limit of 24576 bytes (see [EIP-170](./eip-170.md) and [EIP-3860](./eip-3860.md)). Should this be limiting in the future, a new EOF version could change the format. Besides simplifying client implementations, not using LEB128 also greatly simplifies on-chain parsing. ### Data-only contracts @@ -209,9 +244,11 @@ The EOF prevents deploying contracts with arbitrary bytes (data-only contracts: EF0001 010001 02 00 FE ``` +It is possible in the future that this data will be accessible with data-specific opcodes, such as `DATACOPY` or `EXTDATACOPY`. Until then, callers will need to determine the data offset manually. + ### PC starts with 0 at the code section -The values for `PC` and `JUMP`/`JUMPI` start with 0 and are within the *code* section. We considered keeping `PC`/`JUMP`/`JUMPI` values to operate on the whole *container* and be consistent with `CODECOPY`/`EXTCODECOPY` but in the end decided otherwise. It looks to be much easier to propose EOF extensions that affect jumps and jumpdests when `JUMP`/`JUMPI` already operates on indexes within *code* section only. This also feels more natural and easier to implement in EVM: the new EOF EVM should only care about traversing *code* and accessing other parts of the *container* only on special occasions (e.g. in `CODECOPY` instruction). +The value for `PC` is specified to start at 0 and to be within the active *code* section. We considered keeping `PC` to operate on the whole *container* and be consistent with `CODECOPY`/`EXTCODECOPY` but in the end decided otherwise. This also feels more natural and easier to implement in EVM: the new EOF EVM should only care about traversing *code* and accessing other parts of the *container* only on special occasions (e.g. in `CODECOPY` instruction). ## Backwards Compatibility @@ -221,49 +258,6 @@ The choice of `MAGIC` guarantees that none of the contracts existing on the chai ## Test Cases -### EOF validation - -#### Valid cases - -- Code section without data section -- Code section with data section - -#### Invalid cases - -| Bytecode | Validation error | -|----------------------------------------------------|--------------------------------------------------| -| `EF` | Incomplete magic | -| `EFFF0101000302000400600000AABBCCDD` | Invalid magic | -| `EF00` | No version | -| `EF000001000302000400600000AABBCCDD` | Invalid version | -| `EF000201000302000400600000AABBCCDD` | Invalid version | -| `EF00FF01000302000400600000AABBCCDD` | Invalid version | -| `EF0001` | No header | -| `EF000100` | No code section | -| `EF000101` | No code section size | -| `EF00010100` | Code section size incomplete | -| `EF0001010003` | No section terminator | -| `EF0001010003600000` | No section terminator | -| `EF000101000200` | No code section contents | -| `EF00010100020060` | Code section contents incomplete | -| `EF000101000300600000DEADBEEF` | Trailing bytes after code section | -| `EF000101000301000300600000600000` | Multiple code sections | -| `EF000101000000` | Empty code section | -| `EF000101000002000200AABB` | Empty code section (with non-empty data section) | -| `EF000102000401000300AABBCCDD600000` | Data section preceding code section | -| `EF000102000400AABBCCDD` | Data section without code section | -| `EF000101000202` | No data section size | -| `EF00010100020200` | Data section size incomplete | -| `EF0001010003020004` | No section terminator | -| `EF0001010003020004600000AABBCCDD` | No section terminator | -| `EF000101000302000400600000` | No data section contents | -| `EF000101000302000400600000AABBCC` | Data section contents incomplete | -| `EF000101000302000400600000AABBCCDDEE` | Trailing bytes after data section | -| `EF000101000302000402000400600000AABBCCDDAABBCCDD` | Multiple data sections | -| `EF000101000101000102000102000100FEFEAABB` | Multiple code and data sections | -| `EF000101000302000000600000` | Empty data section | -| `EF0001010002030004006000AABBCCDD` | Unknown section (id = 3) | - ### Contract creation All cases should be checked for creation transaction, `CREATE` and `CREATE2`. @@ -282,8 +276,6 @@ All cases should be checked for creation transaction, `CREATE` and `CREATE2`. ### Contract execution -- Valid EOF code containing `JUMP`/`JUMPI` - offsets relative to code section start are used -- `JUMP`/`JUMPI` to `5B` (`JUMPDEST`) byte outside of code section - exceptional abort - EOF code containing `PC` opcode - offset inside code section is returned - EOF code containing `CODECOPY/CODESIZE` - works as in legacy code - `CODESIZE` returns the size of entire container @@ -300,73 +292,6 @@ All cases should be checked for creation transaction, `CREATE` and `CREATE2`. - `EXTCODECOPY` can copy entire target container - Results don't differ when executed inside legacy or EOF contract -## Reference Implementation - -```python -MAGIC = b'\xEF\x00' -VERSION = 0x01 -S_TERMINATOR = 0x00 -S_CODE = 0x01 -S_DATA = 0x02 - - -# Determines if code is in EOF format of any version. -def is_eof(code: bytes) -> bool: - return code.startswith(MAGIC) - -class ValidationException(Exception): - pass - -# Raises ValidationException on invalid code -def validate_eof(code: bytes): - # Check version - if len(code) < 3 or code[2] != VERSION: - raise ValidationException("invalid version") - - # Process section headers - section_sizes = {S_CODE: 0, S_DATA: 0} - pos = 3 - while True: - # Terminator not found - if pos >= len(code): - raise ValidationException("no section terminator") - - section_id = code[pos] - pos += 1 - if section_id == S_TERMINATOR: - break - - # Disallow unknown sections - if not section_id in section_sizes: - raise ValidationException("invalid section id") - - # Data section preceding code section - if section_id == S_DATA and section_sizes[S_CODE] == 0: - raise ValidationException("data section preceding code section") - - # Multiple sections with the same id - if section_sizes[section_id] != 0: - raise ValidationException("multiple sections with same id") - - # Truncated section size - if (pos + 1) >= len(code): - raise ValidationException("truncated section size") - section_sizes[section_id] = (code[pos] << 8) | code[pos + 1] - pos += 2 - - # Empty section - if section_sizes[section_id] == 0: - raise ValidationException("empty section") - - # Code section cannot be absent - if section_sizes[S_CODE] == 0: - raise ValidationException("no code section") - - # The entire container must be scanned - if len(code) != (pos + section_sizes[S_CODE] + section_sizes[S_DATA]): - raise ValidationException("container size not equal to sum of section sizes") -``` - ## Security Considerations With the anticipated EOF extensions, the validation is expected to have linear computational and space complexity. diff --git a/EIPS/eip-3670.md b/EIPS/eip-3670.md index 5ef9b52f780d6e..469450dc38183a 100644 --- a/EIPS/eip-3670.md +++ b/EIPS/eip-3670.md @@ -45,7 +45,6 @@ The EOF1 format provides following forward compatibility properties: This feature is introduced on the very same block EIP-3540 is enabled, therefore every EOF1-compatible bytecode MUST be validated according to these rules. 1. Previously deprecated instructions `CALLCODE` (0xf2) and `SELFDESTRUCT` (0xff) are invalid and their opcodes are undefined. - 2. At contract creation time *instructions validation* is performed on both *initcode* and *code*. The code is invalid if any of the checks below fails. For each instruction: 1. Check if the opcode is defined. The `INVALID` (0xfe) is considered defined. 2. Check if all instructions' immediate bytes are present in the code (code does not end in the middle of instruction). diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 9cca0550a75133..4d63a37e045b16 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -1,19 +1,19 @@ --- eip: 4750 title: EOF - Functions -description: Individual sections for functions with `CALLF`, `JUMPF` and `RETF` instructions +description: Individual sections for functions with `CALLF` and `RETF` instructions author: Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast) discussions-to: https://ethereum-magicians.org/t/eip-4750-eof-functions/8195 status: Review type: Standards Track category: Core created: 2022-01-10 -requires: 3540, 3670 +requires: 3540, 3670, 5450 --- ## Abstract -Introduce the ability to have several code sections in EOF-formatted ([EIP-3540](./eip-3540.md)) bytecode, each one representing a separate subroutine/function. Two new opcodes,`CALLF` and `RETF`, are introduced to call and return from such a function. Additionally `JUMPF` instruction is introduced to perform a jump to a function. Dynamic jump instructions are disallowed. +Introduce the ability to have several code sections in EOF-formatted ([EIP-3540](./eip-3540.md)) bytecode, each one representing a separate subroutine/function. Two new opcodes,`CALLF` and `RETF`, are introduced to call and return from such a function. Dynamic jump instructions are disallowed. ## Motivation @@ -27,28 +27,15 @@ Furthermore, it aims to improve analysis opportunities by encoding the number of ## Specification -### EOF container changes +### Type Section -1. The requirement of [EIP-3540](./eip-3540.md) "Exactly one code section MUST be present." is relaxed to "At least one code section MUST be present.", i.e. multiple code sections (`kind = 1`) are allowed. -2. Total number of code sections MUST NOT exceed 1024. -3. All code sections MUST precede a data section, if data section is present. -4. New section with `kind = 3` is introduced called the *type section*. -5. Exactly one type section MAY be present. -6. The type section, if present, MUST directly precede all code sections. -7. The type section, if present, contains a sequence of pairs of bytes: first byte in a pair encodes number of inputs, and second byte encodes number of outputs of the code section with the same index. *Note:* This implies that there is a limit of 256 stack for the input and in the output. -8. Therefore, type section size MUST be `n * 2` bytes, where `n` is the number of code sections. -9. First code section MUST have 0 inputs and 0 outputs. -10. Type section MAY be omitted if only a single code. -section is present. In that case it implicitly defines 0 inputs and 0 outputs for this code section. +The type section of EOF containers must adhere to following requirements: -To summarize, a well-formed EOF bytecode will have the following format: +1. The section is comprised of a list of metadata where the metadata index in the type section corresponds to a code section index. Therefore, the type section size MUST be `n * 4` bytes, where `n` is the number of code sections. +2. Each metadata item has 3 attributes: a uint8 `inputs`, a uint8 `outputs`, and a uint16 `max_stack_height`. *Note:* This implies that there is a limit of 255 stack for the input and in the output. This is further restricted to 127 stack items, because the upper bit of both the input and output bytes are reserved for future use. `max_stack_height` is further defined in [EIP-5450](./eip-5450.md). +3. The first code section MUST have 0 inputs and 0 outputs. -``` -bytecode := magic, version, [type_section_header], (code_section_header)+, [data_section_header], 0, [type_section_contents], (code_section_contents)+, [data_section_contents] - -type_section_header := 3, number_of_code_sections * 2 # section kind and size -type_section_contents := 0, 0, code_section_1_inputs, code_section_1_outputs, code_section_2_inputs, code_section_2_outputs, ..., code_section_n_inputs, code_section_n_outputs -``` +Refer to [EIP-3540](./eip-3540.md) to see the full structure of a well-formed EOF bytecode. ### New execution state in EVM @@ -66,15 +53,14 @@ We introduce three new instructions: 1. `CALLF` (`0xb0`) - call a function 2. `RETF` (`0xb1`) - return from a function -3. `JUMPF` (`0xb2`) - jump to a function (without updating return stack) If the code is legacy bytecode, any of these instructions results in an *exceptional halt*. (*Note: This means no change to behaviour.*) First we define several helper values: - `caller_stack_height = return_stack.top().stack_height` - stack height value saved in the top item of return stack -- `type[i].inputs = type_section_contents[i * 2]` - number of inputs of ith section -- `type[i].outputs = type_section_contents[i * 2 + 1]` - number of outputs of ith section +- `type[i].inputs = type_section_contents[i * 4]` - number of inputs of ith section +- `type[i].outputs = type_section_contents[i * 4 + 1]` - number of outputs of ith section If the code is valid EOF1, the following execution rules apply: @@ -96,26 +82,14 @@ If the code is valid EOF1, the following execution rules apply: Under `PC_post_instruction` we mean the PC position after the entire immediate argument of `CALLF`. Data stack height is saved as it was before function inputs were pushed. - *Note:* Code validation rules of [EIP-3670](./eip-3670.md) guarantee there is always an instruction following `CALLF` (since terminating instruction is required to be final one in the section), therefore `PC_post_instruction` always points to an instruction inside section bounds. - + *Note:* Code validation rules of [EIP-5450](./eip-5450.md) guarantee there is always an instruction following `CALLF` (since terminating instruction or unconditional jump is required to be final one in the section), therefore `PC_post_instruction` always points to an instruction inside section bounds. 8. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. -#### `JUMPF` - -1. Has one immediate argument,`code_section_index`, encoded as a 16-bit unsigned big-endian value. -2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. -3. If data stack has more than `caller_stack_height + type[code_section_index].inputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].inputs` items, so that there are exactly `caller_stack_height + type[code_section_index].inputs` items left. -4. Charges 4 gas. -5. Pops nothing and pushes nothing to data stack. -6. Pushes nothing to return stack. -7. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. - #### `RETF` 1. Does not have immediate arguments. -2. If data stack has less than `caller_stack_height + type[code_section_index].outputs` items, execution results in exceptional halt. -3. If data stack has more than `caller_stack_height + type[code_section_index].outputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].outputs` items, so that there are exactly `caller_stack_height + type[code_section_index].outputs` items left. -4. Charges 4 gas. +2. If data stack does not equal `caller_stack_height + type[current_section_index].outputs` items, execution results in exceptional halt. +4. Charges 3 gas. 5. Pops nothing and pushes nothing to data stack. 6. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. 1. If return stack is empty after this, execution halts with success. @@ -125,11 +99,10 @@ If the code is valid EOF1, the following execution rules apply: In addition to container format validation rules above, we extend code section validation rules (as defined in [EIP-3670](./eip-3670.md)). 1. Code validation rules of EIP-3670 are applied to every code section. -2. Code section is invalid in case an immediate argument of any `CALLF` or `JUMPF` is greater than or equal to the total number of code sections. -3. Code section is invalid in case an immediate argument of any `JUMPF` is such that `type[callee_section_index].outputs != type[caller_section_index].outputs`, i.e. it is allowed to only jump to functions with the same output type. -4. `RJUMP`, `RJUMPI` and `RJUMPV` immediate argument value (jump destination relative offset) validation: +2. Code section is invalid in case an immediate argument of any `CALLF` is greater than or equal to the total number of code sections. +3. `RJUMP`, `RJUMPI` and `RJUMPV` immediate argument value (jump destination relative offset) validation: 1. Code section is invalid in case offset points to a position outside of section bounds. - 2. Code section is invalid in case offset points to one of two bytes directly following `CALLF` or `JUMPF` instruction. + 2. Code section is invalid in case offset points to one of two bytes directly following `CALLF` instruction. ### Disallowed instructions @@ -166,202 +139,12 @@ Instead of deprecating `JUMPDEST` we repurpose it as `NOP` instruction, because The purpose of `JUMPDEST` analysis was to find in code the valid `JUMPDEST` bytes that do not happen to be inside `PUSH` immediate data. Only dynamic jump instructions (`JUMP`, `JUMPI`) required destination to be `JUMPDEST` instruction. Relative static jumps (`RJUMP` and `RJUMPI`) do not have this requirement and are validated once at deploy-time EOF instruction validation. Therefore, without dynamic jump instructions, `JUMPDEST` analysis is not required. -### `JUMPF` instruction cleaning the stack - -In case function pushes on the stack more items than inputs required by the jumped-to function, these extra items could be accessed by the jumped-to function, because the underflow check is defined in terms of `caller_stack_height`, which does not change after `JUMPF`: - -> 3. If any instruction would access a data stack item below `caller_stack_height`, execution results in exceptional halt. - -In other words the entire stack frame of the function executing `JUMPF` is accessible to the jumped-to function. - -We believe this introduces unwanted edge-case behaviour with underflow exception depending on how the function was called or jumped-to, and require `JUMPF` to discard extra items to prevent this. - -### `RETF` instruction cleaning the stack - -The stack for the return items for `RETF` is automatically cleaned up. This could be relaxed and the onus could be put onto the user to clean it up, because the stack height difference can be clearly calculated, both when a function is entered via `CALLF` or `JUMPF`. - -However, we hope to relax the clean up requirement for `JUMPF`, and that would mean it is not possible anymore for the user to cleanup for `RETF`, because the height could be different depending on the path a function is entered. - ## Backwards Compatibility This change poses no risk to backwards compatibility, as it is introduced only for EOF1 contracts, for which deploying undefined instructions is not allowed, therefore there are no existing contracts using these instructions. The new instructions are not introduced for legacy bytecode (code which is not EOF formatted). The new execution state and multi-section control flow pose no risk to backwards compatibility, because it is a generalization of executing a single code section. Executing existing contracts (both legacy and EOF1) has no user-observable changes. -## Reference Implementation - -```python -MAGIC = b'\xEF\x00' -VERSION = 0x01 -S_TERMINATOR = 0x00 -S_CODE = 0x01 -S_DATA = 0x02 -S_TYPE = 0x03 - -# The ranges below are as specified in the Yellow Paper. -# Note: range(s, e) excludes e, hence the +1 -valid_opcodes = [ - *range(0x00, 0x0b + 1), - *range(0x10, 0x1d + 1), - 0x20, - *range(0x30, 0x3f + 1), - *range(0x40, 0x48 + 1), - *range(0x50, 0x55 + 1), *range(0x58, 0x5d + 1), - *range(0x60, 0x6f + 1), - *range(0x70, 0x7f + 1), - *range(0x80, 0x8f + 1), - *range(0x90, 0x9f + 1), - *range(0xa0, 0xa4 + 1), - 0xb0, 0xb1, 0xb2, - # Note: 0xfe is considered assigned. - 0xf0, 0xf1, 0xf3, 0xf4, 0xf5, 0xfa, 0xfd, 0xfe -] - -# STOP, RETF, JUMPF, RETURN, REVERT, INVALID -terminating_opcodes = [0x00, 0xb1, 0xb2, 0xf3, 0xfd, 0xfe] - -immediate_sizes = 256 * [0] -immediate_sizes[0x5c] = 2 # RJUMP -immediate_sizes[0x5d] = 2 # RJUMPI -immediate_sizes[0xb0] = 2 # CALLF -immediate_sizes[0xb2] = 2 # JUMPF -for opcode in range(0x60, 0x7f + 1): # PUSH1..PUSH32 - immediate_sizes[opcode] = opcode - 0x60 + 1 - -# Validate EOF code. -# Raises ValidationException on invalid code -def validate_eof(code: bytes): - # Check version - if len(code) < 3 or code[2] != VERSION: - raise ValidationException("invalid version") - - # Process section headers - section_sizes = {S_TYPE: [], S_CODE: [], S_DATA: []} - pos = 3 - while True: - # Terminator not found - if pos >= len(code): - raise ValidationException("no section terminator") - - section_id = code[pos] - pos += 1 - if section_id == S_TERMINATOR: - break - - # Disallow unknown sections - if not section_id in section_sizes: - raise ValidationException("invalid section id") - - # Data section preceding code section (i.e. code section following data section) - if section_id == S_CODE and len(section_sizes[S_DATA]) != 0: - raise ValidationException("data section preceding code section") - - # Code section or data section preceding type section - if section_id == S_TYPE and (len(section_sizes[S_CODE]) != 0 or len(section_sizes[S_DATA]) != 0): - raise ValidationException("code or data section preceding type section") - - # Multiple type or data sections - if section_id == S_TYPE and len(section_sizes[S_TYPE]) != 0: - raise ValidationException("multiple type sections") - if section_id == S_DATA and len(section_sizes[S_DATA]) != 0: - raise ValidationException("multiple data sections") - - # Truncated section size - if (pos + 1) >= len(code): - raise ValidationException("truncated section size") - section_sizes[section_id].append((code[pos] << 8) | code[pos + 1]) - pos += 2 - - # Empty section - if section_sizes[section_id][-1] == 0: - raise ValidationException("empty section") - - # Code section cannot be absent - if len(section_sizes[S_CODE]) == 0: - raise ValidationException("no code section") - - # Not more than 1024 code sections - if len(section_sizes[S_CODE]) > 1024: - raise ValidationException("more than 1024 code sections") - - # Type section can be absent only if single code section is present - if len(section_sizes[S_TYPE]) == 0 and len(section_sizes[S_CODE]) != 1: - raise ValidationException("no obligatory type section") - - # Type section, if present, has size corresponding to number of code sections - if len(section_sizes[S_TYPE]) != 0 and section_sizes[S_TYPE][0] != len(section_sizes[S_CODE]) * 2: - raise ValidationException("invalid type section size") - - # The entire container must be scanned - if len(code) != (pos + sum(section_sizes[S_TYPE]) + sum(section_sizes[S_CODE]) + sum(section_sizes[S_DATA])): - raise ValidationException("container size not equal to sum of section sizes") - - # First type section, if present, has 0 inputs and 0 outputs - if len(section_sizes[S_TYPE]) > 0 and (code[pos] != 0 or code[pos + 1] != 0): - raise ValidationException("invalid type of section 0") - -# Raises ValidationException on invalid code -def validate_code_section(func_id: int, code: bytes, types: list[FunctionType] = [FunctionType(0, 0)]): - # Note that EOF1 already asserts this with the code section requirements - assert len(code) > 0 - - opcode = 0 - pos = 0 - rjumpdests = set() - immediates = set() - while pos < len(code): - # Ensure the opcode is valid - opcode = code[pos] - pos += 1 - if not opcode in valid_opcodes: - raise ValidationException("undefined instruction") - - if opcode == 0x5c or opcode == 0x5d: - if pos + 2 > len(code): - raise ValidationException("truncated relative jump offset") - offset = int.from_bytes(code[pos:pos+2], byteorder = "big", signed = True) - - rjumpdest = pos + 2 + offset - if rjumpdest < 0 or rjumpdest >= len(code): - raise ValidationException("relative jump destination out of bounds") - - rjumpdests.add(rjumpdest) - elif opcode == 0xb0: - if pos + 2 > len(code): - raise ValidationException("truncated CALLF immediate") - section_id = int.from_bytes(code[pos:pos+2], byteorder = "big", signed = False) - - if section_id >= len(types): - raise ValidationException("invalid section id") - elif opcode == 0xb2: - if pos + 2 > len(code): - raise ValidationException("truncated JUMPF immediate") - section_id = int.from_bytes(code[pos:pos+2], byteorder = "big", signed = False) - - if section_id >= len(types): - raise ValidationException("invalid section id") - - if types[section_id].outputs != types[func_id].outputs: - raise ValidationException("incompatible function type for JUMPF") - - # Save immediate value positions - immediates.update(range(pos, pos + immediate_sizes[opcode])) - # Skip immediates - pos += immediate_sizes[opcode] - - # Ensure last opcode's immediate doesn't go over code end - if pos != len(code): - raise ValidationException("truncated immediate") - - # opcode is the *last opcode* - if not opcode in terminating_opcodes: - raise ValidationException("no terminating instruction") - - # Ensure relative jump destinations don't target immediates - if not rjumpdests.isdisjoint(immediates): - raise ValidationException("relative jump destination targets immediate") -``` - ## Security Considerations TBA diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 9a590b80f74211..36b6de2bac0e86 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -29,61 +29,33 @@ In particular, this extended code validation eliminates the need for EVM stack u Code section validation rules as defined by [EIP-3670](./eip-3670.md) (which has been extended by [EIP-4200](./eip-4200.md) and [EIP-4750](./eip-4750.md)) are extended again to include the instruction flow traversal procedure, where every possible code path is examined, and data stack height at each instruction is recorded. -*Data stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual data stack implementation, but only to keep track of its height. Current height value starts at `types[code_section_index].inputs` (number of inputs of this function) at function entry and is updated at each instruction. +*Data stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual data stack implementation, but only to keep track of its height. Current height value starts at `type[code_section_index].inputs` (number of inputs of this function) at function entry and is updated at each instruction. At the same time the following properties are being verified: 1. For each reachable instruction in the section, data stack height is the same for all possible code paths going through this instruction. 2. For each instruction, data stack always has enough items, i.e. stack underflow is invalid. 3. For `CALLF` instruction, data stack has enough items to use as input arguments to a called function according to its type defined in the type section. -4. For every terminating instruction, except `RETF`, data stack is empty after executing it. -5. For `RETF` instruction, data stack before executing it has exactly `n` items to use as output values returned from a function, where `n` is function's number of outputs according to its type defined in the type section. -6. Maximum data stack height required by a function does not exceed `1024`. +4. For `RETF` instruction, data stack before executing it has exactly `n` items to use as output values returned from a function, where `n` is function's number of outputs according to its type defined in the type section. +5. `max_stack_height` does not exceed 1023. +6. The maximum data stack height matches the corresponding code section's `max_stack_height` within the type section body. +7. No unreachable instructions exist in the code section. -To examine every reachable code path, validation needs to traverse every instruction in order, while also following each non-conditional jump, and following both possible branches for each conditional jump. See below for reference implementation. +To examine every reachable code path, validation needs to traverse every instruction in order, while also following each non-conditional jump, and following both possible branches for each conditional jump. After completing this, verify each instruction was visited at least once. Fail if any instructions were not visited as this invalidates 7). The complexity of this traversal is linear in the number of instructions, because each code path is examined only once, and property 1 guarantees no loops in the validation. ### Execution -Given new deploy-time guarantees, EVM implementation is not required anymore to have run-time stack underflow check for each executed instruction. However implementations can keep it if they choose to do so. +Given new deploy-time guarantees, EVM implementation is not required anymore to have run-time stack underflow check for each executed instruction. Stack overflow check, on the other hand, is still required at run-time, because function execution can start at arbitrary (i.e. known only at run-time) stack height at `CALLF` instruction of a caller (i.e. each execution can be in arbitrary inner call frame). Verification algorithm examines only stack height changes relative to starting stack height of the function. -#### JUMPF changes - -In case a function pushed more items to the stack than is required as inputs by the jumped-to function, the previously defined `JUMPF` instruction behaviour was to remove the extra items: - -> 3. If data stack has more than `caller_stack_height + type[code_section_index].inputs` items, discards the items between `caller_stack_height` and top `type[code_section_index].inputs`, so that there are exactly `caller_stack_height + type[code_section_index].items` items left. - -Given the new deploy-time guarantee of no function underflowing its stack frame, `JUMPF` instruction can check only that there is enough items for input arguments of the callee on the stack, without making sure there is exactly `inputs` items and not more. - -Therefore, the previously defined behavior of discarding extra items is removed, and only the check for enough inputs is done: - -> 2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. - -With this change `JUMPF` operation complexity does not depend on `ouputs` value and is constant-time, therefore the price of `JUMPF` is lowered to 3 gas. - ## Rationale ### Stack overflow check only in CALLF -In the current proposal stack overflow checks are unchanged (i.e. are done for every instruction). However, we can provide more efficient variant where stack overflow check is performed only in `CALLF` instruction and uses called function's `max_stack_height` information for this. This decreases flexibility of an EVM program because `max_stack_height` corresponds to the worst-case control-flow path in the function. -Moreover, the `max_stack_height` computed during validation must be stored alongside the code. This can be in the EOF itself or in implementation-defined format. - -### Unreachable code - -The current validation algorithm ignores *unreachable* instructions. The algorithm can be extended to reject any code having any unreachable instructions but additional instructions traversal is needed (or more efficient algorithm must be developed). - -### Clean stack upon termination - -It is currently required that the EVM stack is empty (in the current function context) after any terminating instruction (`STOP`, `RETURN`, etc, but also `RETF`). This requirement can be lifted. - -This can be used for implementing more efficient early exits from a function (e.g. assertion failure). - -For "exit" instructions which terminates the whole program execution (`STOP`, `RETURN`, etc) this is no change comparing to pre-EOF EVM. I.e. some *garbage* can be left on the stack. Cleaning the stack does not improve EVM implementation performance but makes the EVM programs potentially cost more (compiler is required to insert additional `POP` instructions). - -For `RETF` semantic would be more complicated. For `n` function outputs and `s` the stack height at `RETF` the EVM must erase `s-n` non-top stack items and move the `n` stack items to the place of erased ones. Cost of such operation may be relatively cheap but is not constant. +In this EIP, we provide a more efficient variant of the EVM where stack overflow check is performed only in `CALLF` instruction using the called function's `max_stack_height` information. This decreases flexibility of an EVM program because `max_stack_height` corresponds to the worst-case control-flow path in the function. ## Backwards Compatibility @@ -91,88 +63,6 @@ This change requires a “network upgrade”, since it modifies consensus rules. It poses no risk to backwards compatibility, as it is introduced only for EOF1 contracts, for which deploying undefined instructions is not allowed, therefore there are no existing contracts using these instructions. The new instructions are not introduced for legacy bytecode (code which is not EOF formatted). -## Reference Implementation - -```python -# Returns maximum stack height required by function execution frame -# (not including frames of internal calls) -# Raises ValidateExceptin if code is invalid. -def validate_function(func_id: int, code: bytes, types: list[FunctionType] = [FunctionType(0, 0)]) -> int: - assert func_id >= 0 - assert types[func_id].inputs >= 0 - assert types[func_id].outputs >= 0 - - validate_code_section(code, len(types)) - - stack_heights = {} - start_stack_height = types[func_id].inputs - max_stack_height = start_stack_height - - # queue of instructions to analyze, list of (pos, stack_height) pairs - worklist = [(0, start_stack_height)] - - while worklist: - pos, stack_height = worklist.pop(0) - while True: - # Assuming code ends with a terminating instruction due to previous validation in validate_code_section() - assert pos < len(code), "code is invalid" - op = code[pos] - info = TABLE[op] - - # Check if stack height (type arity) at given position is the same - # for all control flow paths reaching this position. - if pos in stack_heights: - if stack_height != stack_heights[pos]: - raise ValidationException("stack height mismatch for different paths") - else: - break - else: - stack_heights[pos] = stack_height - - - stack_height_required = info.stack_height_required - stack_height_change = info.stack_height_change - - if op == OP_CALLF: - called_func_id = int.from_bytes(code[pos + 1:pos + 3], byteorder="big", signed=False) - # Assuming called_func_id is valid due to previous validation in validate_code_section() - stack_height_required += types[called_func_id].inputs - stack_height_change += types[called_func_id].outputs - types[called_func_id].inputs - - # Detect stack underflow - if stack_height < stack_height_required: - raise ValidationException("stack underflow") - - stack_height += stack_height_change - max_stack_height = max(max_stack_height, stack_height) - - # Handle jumps - if op == OP_RJUMP: - offset = int.from_bytes(code[pos + 1:pos + 3], byteorder="big", signed=True) - pos += info.immediate_size + 1 + offset # pos is valid for validated code. - - elif op == OP_RJUMPI: - offset = int.from_bytes(code[pos + 1:pos + 3], byteorder="big", signed=True) - # Save True branch for later and continue to False branch. - worklist.append((pos + 3 + offset, stack_height)) - pos += info.immediate_size + 1 - - elif info.is_terminating: - expected_height = types[func_id].outputs if op == OP_RETF else 0 - if stack_height != expected_height: - raise ValidationException("non-empty stack on terminating instruction") - break - - else: - pos += info.immediate_size + 1 - - - if max_stack_height > 1024: - raise ValidationException("max stack above limit") - - return max_stack_height -``` - ## Security Considerations Needs discussion. From 2f369da529eb083f008098779c6a27aeb79adc74 Mon Sep 17 00:00:00 2001 From: Steven Pineda Date: Wed, 21 Dec 2022 17:07:02 -0500 Subject: [PATCH 068/274] Fixes bug when replacing asset on sample implementation of 5773 (#6202) * Fixes bug when replacing asset on sample implementation of 5773. * Rename the smart contract in specification The smart contract specification was renamed to be identic to the name of the proposal. This has been done so that the assets changes can be merged. Co-authored-by: Jan Turk --- EIPS/eip-5773.md | 2 +- assets/eip-5773/contracts/MultiAssetToken.sol | 2 +- assets/eip-5773/test/multiasset.ts | 36 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index bb146edc1d6f51..23114adaa37b6e 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -73,7 +73,7 @@ Alternative example of this, could be version control of an IoT device's firmwar 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 -/// @title EIP-5773 Multi-Asset context-dependent tokens +/// @title EIP-5773 Context-Dependent Multi-Asset Tokens /// @dev See https://eips.ethereum.org/EIPS/eip-5773 /// @dev Note: the ERC-165 identifier for this interface is 0xd1526708. diff --git a/assets/eip-5773/contracts/MultiAssetToken.sol b/assets/eip-5773/contracts/MultiAssetToken.sol index 86d051ede66c80..f35520bd1247e0 100644 --- a/assets/eip-5773/contracts/MultiAssetToken.sol +++ b/assets/eip-5773/contracts/MultiAssetToken.sol @@ -468,7 +468,7 @@ contract MultiAssetToken is Context, IERC721, IMultiAsset { if (replacefound) { // We don't want to remove and then push a new asset. // This way we also keep the priority of the original resource - _activeAssets[tokenId][index] = assetId; + _activeAssets[tokenId][replaceIndex] = assetId; delete _tokenAssets[tokenId][replacesId]; } else { // We use the current size as next priority, by default priorities would be [0,1,2...] diff --git a/assets/eip-5773/test/multiasset.ts b/assets/eip-5773/test/multiasset.ts index 0685a44e78792e..54a3b5293713ec 100644 --- a/assets/eip-5773/test/multiasset.ts +++ b/assets/eip-5773/test/multiasset.ts @@ -370,6 +370,42 @@ describe('MultiAsset', async () => { metaURIDefault, ]); }); + + it("can overwrite an existing asset after 3 have been added and 1 accepted", async function () { + const resId = 1; + const resId2 = 2; + const resId3 = 3; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2, resId3]); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.emit( + token, + "AssetAddedToToken" + ); + await expect(token.addAssetToToken(tokenId, resId2, 0)).to.emit( + token, + "AssetAddedToToken" + ); + await expect(token.addAssetToToken(tokenId, resId3, resId2)) + .to.emit(token, "AssetAddedToToken") + .withArgs(tokenId, resId3, resId2); + + const pendingIds = await token.getPendingAssets(tokenId); + + expect( + await renderUtils.getAssetsById(token.address, tokenId, pendingIds) + ).to.be.eql([metaURIDefault, metaURIDefault, metaURIDefault]); + + await expect(token.acceptAsset(tokenId, 1, resId2)) + .to.emit(token, "AssetAccepted") + .withArgs(tokenId, resId2, 0); + + await expect(token.acceptAsset(tokenId, 1, resId3)) + .to.emit(token, "AssetAccepted") + .withArgs(tokenId, resId3, 2); + }); + }); describe('Rejecting assets', async function () { From 6f1bac8d1a73bf9ba372f4a2e95da153c53087ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Wed, 21 Dec 2022 23:45:50 +0100 Subject: [PATCH 069/274] 4750,5450: Specify stack overflow check only in CALLF (#6205) * 4750,5450: Specify stack overflow check only in CALLF * Apply suggestions from code review Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> --- EIPS/eip-4750.md | 2 +- EIPS/eip-5450.md | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 4d63a37e045b16..e3e67a7674c160 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -68,7 +68,7 @@ If the code is valid EOF1, the following execution rules apply: 1. Has one immediate argument,`code_section_index`, encoded as a 16-bit unsigned big-endian value. 2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. -3. If data stack size after the call would exceed `1024` items, (i.e. if `caller_stack_height - type[code_section_index].inputs + type[code_section_index].ouputs > 1024`), execution results in exceptional halt. +3. If data stack size exceeds `1024 - type[code_section_index].max_stack_height` (i.e. if the called function may exceed the global stack height limit), execution results in exceptional halt. This also guarantees that the stack height after the call is within the limits. 4. If return stack already has `1024` items, execution results in exceptional halt. 5. Charges 5 gas. 6. Pops nothing and pushes nothing to data stack. diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 36b6de2bac0e86..5a6c369b575a4f 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -13,13 +13,13 @@ requires: 3540, 3670, 4200, 4750 ## Abstract -Introduce extended validation of code sections to guarantee that stack underflow cannot happen during execution of validated contracts. +Introduce extended validation of code sections to guarantee that neither stack underflow nor overflow can happen during execution of validated contracts. ## Motivation Currently existing EVM implementations perform a number of validity checks for each executed instruction, such as check for stack overflow/underflow, sufficient gas, etc. This change aims to minimize the number of such checks required at run-time, by verifying at deploy-time that no exceptional conditions can happen, and preventing to deploy the code where it could happen. -In particular, this extended code validation eliminates the need for EVM stack underflow checks done for every executed instruction. It also prevents deploying code that can be statically proven to require more than 1024 stack items, however it may still be possible to exceed that limit in some cases, and thus overflow checks cannot be fully eliminated. +In particular, this extended code validation eliminates the need for EVM stack underflow checks done for every executed instruction. It also prevents deploying code that can be statically proven to require more than 1024 stack items and limits stack overflow checks to the `CALLF` instruction only. ## Specification @@ -47,9 +47,7 @@ The complexity of this traversal is linear in the number of instructions, becaus ### Execution -Given new deploy-time guarantees, EVM implementation is not required anymore to have run-time stack underflow check for each executed instruction. - -Stack overflow check, on the other hand, is still required at run-time, because function execution can start at arbitrary (i.e. known only at run-time) stack height at `CALLF` instruction of a caller (i.e. each execution can be in arbitrary inner call frame). Verification algorithm examines only stack height changes relative to starting stack height of the function. +Given new deploy-time guarantees, EVM implementation is not required anymore to have run-time stack underflow nor overflow checks for each executed instruction. The exception is the `CALLF` performing data stack overflow check for the entire called function. ## Rationale From accf816d3614446b747e2c88a9b42ee368017525 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Thu, 22 Dec 2022 11:18:20 -0700 Subject: [PATCH 070/274] Add EIP-6206: EOF - JUMPF instruction (#6206) * add jumpf eip * add eip number and discussion link * Fix markdownlint errors * Last markdownlint error * Fix invalid link * Formatting stuff Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6206.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 EIPS/eip-6206.md diff --git a/EIPS/eip-6206.md b/EIPS/eip-6206.md new file mode 100644 index 00000000000000..7252e628ed0201 --- /dev/null +++ b/EIPS/eip-6206.md @@ -0,0 +1,58 @@ +--- +eip: 6206 +title: EOF - JUMPF instruction +description: Introduces instruction for chaining function calls. +author: Andrei Maiboroda (@gumb0), Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Matt Garnett (@lightclient) +discussions-to: https://ethereum-magicians.org/t/eip-4750-eof-functions/8195 +status: Draft +type: Standards Track +category: Core +created: 2022-12-21 +requires: 4750, 5450 +--- + +## Abstract + +This EIP allows for tail call optimizations in EOF functions ([EIP-4750](./eip-4750.md)) by introducing a new instruction `JUMPF`, which jumps to a code section without adding a new return stack frame. + +## Motivation + +It is common for functions to make a call at the end of the routine only to then return. `JUMPF` optimizes this behavior by changing code sections without needing to update the return stack. + +## Specification + +A new instruction, `JUMPF (0xb2)`, is introduced. + +### Execution Semantics + +1. `JUMPF` has one immediate argument, `code_section_index`, encoded as a 16-bit unsigned big-endian value. +2. If the data stack size exceeds `1024 - type[code_section_index].max_stack_height` (i.e. if the called function may exceed the global stack height limit), execution results in an exceptional halt. This guarantees that the stack height after the call is within the limits. +3. `JUMPF` costs 5 gas. +4. `JUMPF` neither pops nor pushes anything to the data stack. + +### Code Validation + +Let the definition of `type[i]` be inherited from [EIP-4750](./eip-4750.md) and define `stack_height` to be the height of the stack at a certain instruction during the instruction flow traversal if the data stack at the start of the function were equal to `type[i].inputs`. + +* The immediate argument of `JUMPF` MUST be greater than or equal to the total number of code sections. +* The stack height at `JUMPF` MUST be greater than or equal to `type[code_section_index].inputs`. +* `type[current_section_index].outputs` MUST equal `stack_height - type[code_section_index].inputs + type[code_section_index].outputs`. This means that `code_section_index` can output less stack elements than the original code section called by the top element on the return stack if the `current_section_index` code section leaves the delta `type[current_section_index] - type[code_section_index]` element(s) on the stack. +* The code validation defined in [EIP-4200](./eip-4200.md) also fails if any `RJUMP*` offset points to one of the two bytes directly following a `JUMPF` instruction. + +## Rationale + +### Allowing `JUMPF` to section with less outputs + +As long as `JUMPF` prepares the delta `type[current_section_index] - type[code_section_index]` stack elements before changing code sections, it is possible to jump to a section with less outputs than was originally entered via `CALLF`. This will reduce duplicated code as it will allow compilers more flexibility during code generation such that certain helpers can be used generically by functions, regardless of their output values. + +## Backwards Compatibility + +This change is backward compatible as EOF does not allow undefined instructions to be used or deployed, meaning no contracts will be affected. + +## Security Considerations + +Needs discussion. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From dc7f4345858793b506e7685bd4bb143a9846d0cb Mon Sep 17 00:00:00 2001 From: Anton Buenavista Date: Thu, 22 Dec 2022 14:19:06 -0500 Subject: [PATCH 071/274] Update EIP-5115: Updates to interface based on community feedback (#6207) * Update EIP-5115: Changes based from community feedback * Update EIP-5115: Changes in interface and definitions * Update EIP-5115: Linting fixes --- EIPS/eip-5115.md | 284 +++++++++++++++++++---------------------------- 1 file changed, 114 insertions(+), 170 deletions(-) diff --git a/EIPS/eip-5115.md b/EIPS/eip-5115.md index 480b2752b5fd3d..e86ef8a1a3c539 100644 --- a/EIPS/eip-5115.md +++ b/EIPS/eip-5115.md @@ -1,9 +1,9 @@ --- eip: 5115 -title: Super Composable Yield Token -description: Interface for wrapped yield-generating tokens. +title: SY Token +description: Interface for wrapped yield-bearing tokens. author: Vu Nguyen (@mrenoon), Long Vuong (@UncleGrandpa925), Anton Buenavista (@ayobuenavista) -discussions-to: https://ethereum-magicians.org/t/eip-5115-super-composable-yield-token-standard/9423 +discussions-to: https://ethereum-magicians.org/t/eip-5115-super-composable-yield-token-standard/9423 status: Draft type: Standards Track category: ERC @@ -13,31 +13,34 @@ requires: 20 ## Abstract -This standard proposes an API for wrapped yield-generating tokens within smart contracts. It is an extension on the [EIP-20](./eip-20.md) token that provides basic functionality for transferring, depositing, withdrawing tokens, as well as reading balances. +This standard proposes an API for wrapped yield-bearing tokens within smart contracts. It is an extension on the [EIP-20](./eip-20.md) token that provides basic functionality for transferring, depositing, withdrawing tokens, as well as reading balances. ## Motivation Yield generating mechanisms are built in all shapes and sizes, necessitating a manual integration every time a protocol builds on top of another protocol’s yield generating mechanism. -[EIP-4626](./eip-4626.md) tackled a significant part of this fragmentation by standardizing the interfaces for vaults, a major category among various yield-generating mechanisms. +[EIP-4626](./eip-4626.md) tackled a significant part of this fragmentation by standardizing the interfaces for vaults, a major category among various yield generating mechanisms. In this EIP, we’re extending the coverage to include assets beyond EIP-4626’s reach, namely: -- Yield-generating assets that have different base tokens used for minting vs accounting for the pool value. - - This category includes AMM liquidity tokens (which are yield generating assets that yield swap fees) since the value of the pool is measured in “liquidity units” (for example, $\sqrt k$ in UniswapV2, as defined in UniswapV2 whitepaper) which can’t be deposited in (as they are not tokens). + +- yield-bearing assets that have different input tokens used for minting vs accounting for the pool value. + - This category includes AMM liquidity tokens (which are yield-bearing assets that yield swap fees) since the value of the pool is measured in “liquidity units” (for example, $\sqrt k$ in UniswapV2, as defined in UniswapV2 whitepaper) which can’t be deposited in (as they are not tokens). - This extends the flexibility in minting the yield-bearing assets. For example, there could be an ETH vault that wants to allow users to deposit cETH directly instead of ETH, for gas efficiency or UX reasons. - Assets with reward tokens by default (e.g. COMP rewards for supplying in Compound). The reward tokens are expected to be sold to compound into the same asset. +- This EIP can be extended further to include the handling of rewards, such as the claiming of accrued multiple rewards tokens. -While EIP-4626 is a well-designed and suitable standard for most vaults, there will inevitably be some yield-generating mechanisms that do not fit into their category (LP tokens for instance). A more flexible standard is required to standardize the interaction with all types of yield generating mechanisms. +While EIP-4626 is a well-designed and suitable standard for most vaults, there will inevitably be some yield generating mechanisms that do not fit into their category (LP tokens for instance). A more flexible standard is required to standardize the interaction with all types of yield generating mechanisms. -Therefore, we are proposing Super Composable Yield (SCY), a flexible standard for wrapped yield generating tokens that could cover most mechanisms in DeFi. We foresee that: +Therefore, we are proposing Standardized Yield (SY), a flexible standard for wrapped yield-bearing tokens that could cover most mechanisms in DeFi. We foresee that: - EIP-4626 will still be a popular vault standard, that most vaults should adopt. -- SCY tokens can wrap over most yield generating mechanisms in DeFi, including EIP-4626 vaults for projects built on top of interest-bearing tokens. -- Whoever needs the functionalities of SCY could integrate with the existing SCY tokens or write a new SCY (to wrap over the target interest-bearing token). +- SY tokens can wrap over most yield generating mechanisms in DeFi, including EIP-4626 vaults for projects built on top of yield-bearing tokens. +- Whoever needs the functionalities of SY could integrate with the existing SY tokens or write a new SY (to wrap over the target yield-bearing token). +- Reward handling can be extended from the SY token. ### Use Cases -This EIP is designed for flexibility, aiming to accommodate as many yield generating mechanisms as possible. Particularly, this standard aims to be generalized enough that it supports the following uses cases and more: +This EIP is designed for flexibility, aiming to accommodate as many yield generating mechanisms as possible. Particularly, this standard aims to be generalized enough that it supports the following use cases and more: - Money market supply positions - Lending DAI in Compound, getting DAI interests and COMP rewards @@ -57,19 +60,18 @@ This EIP is designed for flexibility, aiming to accommodate as many yield genera - Provide LOOKS in LooksRare, getting LOOKS yield and WETH rewards - Rebasing tokens - Stake OHM into sOHM/gOHM, getting OHM rebase yield - - Stake BTRFLY into xBTRFLY, getting BTRFLY rebase yield -The EIP hopes to minimize, if not possibly eliminate, the use of customized adapters in order to interact with many different forms of yield-generating token mechanisms. +The EIP hopes to minimize, if not possibly eliminate, the use of customized adapters in order to interact with many different forms of yield-bearing token mechanisms. ## Specification ### Generic Yield Generating Pool -We will first introduce Generic Yield Generating Pool (GYGP), a model to describe most yield generating mechanisms in DeFi. In every yield generating mechanism, there is a pool of funds, whose value is measured in **assets**. There are a number of users who contribute liquidity to the pool, in exchange for **shares** of the pool, which represents units of ownership of the pool. Over time, the value (measured in **assets**) of the pool grows, such that each **share** is worth more **assets** over time. The pool could earn a number of **reward tokens** over time, which are distributed to the users according to some logic (For example, proportionally the number of **shares**). +We will first introduce Generic Yield Generating Pool (GYGP), a model to describe most yield generating mechanisms in DeFi. In every yield generating mechanism, there is a pool of funds, whose value is measured in **assets**. There are a number of users who contribute liquidity to the pool, in exchange for **shares** of the pool, which represents units of ownership of the pool. Over time, the value (measured in **assets**) of the pool grows, such that each **share** is worth more **assets** over time. The pool could earn a number of **reward tokens** over time, which are distributed to the users according to some logic (for example, proportionally the number of **shares**). Here are the more concrete definitions of the terms: -#### Definitions: +#### GYGP Definitions: - **asset**: Is a unit to measure the value of the pool. At time *t*, the pool has a total value of *TotalAsset(t)* **assets**. - **shares**: Is a unit that represents ownership of the pool. At time *t*, there are *TotalShares(t)* **shares** in total. @@ -79,10 +81,10 @@ Here are the more concrete definitions of the terms: #### State changes: -1. A user deposits **assets** into the pool, in exchange for new **shares** that will be created for the user, proportionally to the asset amount being deposited compared to the value of the pool. -2. A user withdraws **assets** from the pool, by burning their **shares**, proportionally to the asset amount being burned compared to the value of the pool -3. The pool earns some **assets**. The **exchange rate** will simply increase due to the additional **assets.** -4. The pool earns some **reward tokens**. The additional reward tokens will be distributed among the users. +1. A user deposits $d_a$ **assets** into the pool at time $t$ ($d_a$ could be negative, which means a withdraw from the pool). $d_s = d_a / ExchangeRate(t)$ new **shares** will be created and given +to user (or removed and burned from the user when $d_a$ is negative). +2. The pool earns $d_a$ (or loses $−d_a$ if $d_a$ is negative) **assets** at time $t$. The **exchange rate** simply increases (or decreases if $d_a$ is negative) due to the additional assets. +3. The pool earns $d_r$ **reward token** $i$. Every user will receive a certain amount of **reward token** $i$. #### Examples of GYGPs in DeFi: @@ -90,39 +92,41 @@ Here are the more concrete definitions of the terms: | --- | --- | --- | --- | --- | | Supply USDC in Compound | USDC | cUSDC | COMP | USDC value per cUSDC, increases with USDC supply interests | | ETH liquid staking in Lido | stETH | wstETH | None | stETH value per wstETH, increases with ETH staking rewards | -| Stake LOOKS in LooksRare | LOOKS | shares (in contract) | WETH | LOOKS value per shares, increases with LOOKS rewards | -| Stake BTRFLY into xBTRFLY | BTRFLY | xBTRFLY | None | BTRFLY value per xBTRFLY, increases due to rebase rewards | +| Stake LOOKS in LooksRare Compounder | LOOKS | shares (in contract) | WETH | LOOKS value per shares, increases with LOOKS rewards | +| Stake APE in $APE Compounder | sAPE | shares (in contract) | APE | sAPE value per shares, increases with APE rewards | | Provide ETH+USDC liquidity on Sushiswap | ETHUSDC liquidity (a pool of x ETH + y USDC has sqrt(xy) ETHUSDC liquidity) | ETHUSDC Sushiswap LP (SLP) token | None | ETHUSDC liquidity value per ETHUSDC SLP, increases due to swap fees | | Provide ETH+USDC liquidity on Sushiswap and stake into Onsen | ETHUSDC liquidity (a pool of x ETH + y USDC has sqrt(xy) ETHUSDC liquidity) | ETHUSDC Sushiswap LP (SLP) token | SUSHI | ETHUSDC liquidity value per ETHUSDC SLP, increases due to swap fees | -| Provide USDC+USDT+DAI liquidity in Curve | 3crv pool’s liquidity (amount of D per 3crv token) | 3crv token | CRV | 3crv pool’s liquidity per 3crv token, increases due to swap fees | | Provide BAL+WETH liquidity in Balancer (80% BAL, 20% WETH) | BALWETH liquidity (a pool of x BAL + y WETH has x^0.8*y^0.2 BALWETH liquidity) | BALWETH Balancer LP token | None | BALWETH liquidity per BALWETH Balancer LP token, increases due to swap fees | +| Provide USDC+USDT+DAI liquidity in Curve | 3crv pool’s liquidity (amount of D per 3crv token) | 3crv token | CRV | 3crv pool’s liquidity per 3crv token, increases due to swap fees | +| Provide FRAX+USDC liquidity in Curve then stake LP in Convex | BALWETH liquidity (a pool of x BAL + y WETH has x^0.8*y^0.2 BALWETH liquidity) | BALWETH Balancer LP token | None | BALWETH liquidity per BALWETH Balancer LP token, increases due to swap fees | + -### Super Composable Yield Token Standard +### Standardized Yield Token Standard -#### Overview +#### Overview: -Super Composable Yield is a token standard for all GYGPs. Each Super Composable Yield token represents **shares** in a GYGP and allows for interacting with the GYGP via a standard interface +Standardized Yield (SY) is a token standard for any yield generating mechanism that conforms to the GYGP model. Each SY token represents **shares** in a GYGP and allows for interacting with the GYGP via a standard interface. -All SCY tokens: +All SY tokens: - **MUST** implement **`EIP-20`** to represent shares in the underlying GYGP. - **MUST** implement EIP-20’s optional metadata extensions `name`, `symbol`, and `decimals`, which **SHOULD** reflect the underlying GYGP’s accounting asset’s `name`, `symbol`, and `decimals`. -- **MAY** implement [EIP-2612](./eip-2612.md) to improve the UX of approving SCY tokens on various integrations. -- **MAY** revert on calls to `transfer` and `transferFrom` if a SCY token is to be non-transferable. +- **MAY** implement [EIP-2612](./eip-2612.md) to improve the UX of approving SY tokens on various integrations. +- **MAY** revert on calls to `transfer` and `transferFrom` if a SY token is to be non-transferable. - The EIP-20 operations `balanceOf`, `transfer`, `totalSupply`, etc. **SHOULD** operate on the GYGP “shares”, which represent a claim to ownership on a fraction of the GYGP’s underlying holdings. -#### Definition of base tokens +#### SY Definitions: -Base tokens are tokens that could be deposited to mint SCY tokens (and hence, to enter the underlying GYGP), or redeemed when burning SCY tokens (and hence, exiting the underlying GYGP). Essentially, base tokens are implicitly converted into units of **assets** when deposited or redeemed, to conform to state change #1 and #2 of the GYGP definition above. +On top of the definitions above for GYGPs, we need to define 2 more concepts: -There could be multiple kinds of base tokens in a SCY. This allows for maximum flexibility in how to mint SCY to enter the underlying yield generating pool. For example, both BAL and WETH (and even BALWETH LP token) could be used to mint the SCY token for BALWETH Balancer pool (by providing single-sided liquidity). +- **input tokens**: Are tokens that can be converted into assets to enter the pool. Each SY can accept several possible input tokens $tokens_{in_{i}}$ -As such, base tokens are not necessarily the same as the asset (which is a key difference between SCY and EIP-4626). +- **output tokens**: Are tokens that can be redeemed from assets when exiting the pool. Each SY can have several possible output tokens $tokens_{out_{i}}$ #### Interface ```solidity -interface ISuperComposableYield { +interface IStandardizedYield { enum AssetType { TOKEN, LIQUIDITY @@ -133,62 +137,50 @@ interface ISuperComposableYield { address indexed receiver, address indexed tokenIn, uint256 amountDeposited, - uint256 amountScyOut + uint256 amountSyOut ); event Redeem( address indexed caller, address indexed receiver, address indexed tokenOut, - uint256 amountScyToRedeem, + uint256 amountSyToRedeem, uint256 amountTokenOut ); - event ClaimRewards( - address indexed caller, - address indexed user, - address[] rewardTokens, - uint256[] rewardAmounts - ); - - event ExchangeRateUpdated(uint256 oldExchangeRate, uint256 newExchangeRate); - function deposit( address receiver, address tokenIn, - uint256 amountTokenToPull, - uint256 minSharesOut + uint256 amountTokenToDeposit, + uint256 minSharesOut, + bool depositFromInternalBalance ) external returns (uint256 amountSharesOut); function redeem( address receiver, - uint256 amountSharesToPull, + uint256 amountSharesToRedeem, address tokenOut, - uint256 minTokenOut + uint256 minTokenOut, + bool burnFromInternalBalance ) external returns (uint256 amountTokenOut); - function claimRewards(address user) external returns (uint256[] memory rewardAmounts); - - function exchangeRateCurrent() external returns (uint256); + function exchangeRate() external view returns (uint256 res); - function exchangeRateStored() external view returns (uint256); + function getTokensIn() external view returns (address[] memory res); - function getRewardTokens() external view returns (address[] memory); - - function getBaseTokens() external view returns (address[] memory); + function getTokensOut() external view returns (address[] memory res); function yieldToken() external view returns (address); - function isValidBaseToken(address token) external view returns (bool); + function previewDeposit(address tokenIn, uint256 amountTokenToDeposit) + external + view + returns (uint256 amountSharesOut); - function assetInfo() - external - view - returns ( - AssetType assetType, - address assetAddress, - uint8 assetDecimals - ); + function previewRedeem(address tokenOut, uint256 amountSharesToRedeem) + external + view + returns (uint256 amountTokenOut); function name() external view returns (string memory); @@ -204,126 +196,105 @@ interface ISuperComposableYield { function deposit( address receiver, address tokenIn, - uint256 amountTokenToPull, - uint256 minSharesOut + uint256 amountTokenToDeposit, + uint256 minSharesOut, + bool depositFromInternalBalance ) external returns (uint256 amountSharesOut); ``` -This method will first pull `amountTokenToPull` of `tokenIn` (a **base token**), and use the floating amount `tokenIn` in the SCY contract to deposit to mint new **shares**. +This function will deposit *amountTokenToDeposit* of input token $i$ (*tokenIn*) to mint new SY shares. -The ideal way to deposit is to send `tokenIn` in first, then call the `deposit` function with `amountTokenToPull = 0`. This pattern is similar to UniswapV2 (and UniswapV3) pools, which allow for better composability and gas efficiency by minimizing token transfers. For example, a router contract could swap from some other token to `tokenIn` which is sent directly to the SCY contract before `deposit` is called. +If *depositFromInternalBalance* is set to *false*, msg.sender will need to initially deposit *amountTokenToDeposit* of input token $i$ (*tokenIn*) into the SY contract, then this function will convert the *amountTokenToDeposit* of input token $i$ into $d_a$ worth of **asset** and deposit this amount into the pool for the *receiver*, who will receive *amountSharesOut* of SY tokens (**shares**). If *depositFromInternalBalance* is set to *true*, then *amountTokenToDeposit* of input token $i$ (*tokenIn*) will be taken from receiver directly (as msg.sender), and will be converted and shares returned to the receiver similarly to the first case. -This function will convert the amount of `tokenIn` into some worth of **assets** and deposit this amount into the SCY contract for the recipient, who will receive `amountSharesOut` of SCY tokens (**shares**). +This function should revert if $amountSharesOut \lt minSharesOut$. - **MUST** emit the `Deposit` event. -- **MUST** support EIP-20’s `approve` / `transferFrom` flow where `tokenIn` are taken from receiver directly (as msg.sender) or if the msg.sender has EIP-20 approved allowance over the base token of the receiver. -- **MUST** revert if $amountSharesOut \lt minSharesOut$ (due to deposit limit being reached, slippage, or the user not approving enough `tokenIn` **to the SCY contract, etc). +- **MUST** support EIP-20’s `approve` / `transferFrom` flow where `tokenIn` are taken from receiver directly (as msg.sender) or if the msg.sender has EIP-20 approved allowance over the input token of the receiver. +- **MUST** revert if $amountSharesOut \lt minSharesOut$ (due to deposit limit being reached, slippage, or the user not approving enough `tokenIn` **to the SY contract, etc). +- **MAY** be payable if the `tokenIn` depositing asset is the chain's native currency (e.g. ETH). ```solidity function redeem( address receiver, - uint256 amountSharesToPull, + uint256 amountSharesToRedeem, address tokenOut, - uint256 minTokenOut + uint256 minTokenOut, + bool burnFromInternalBalance ) external returns (uint256 amountTokenOut); ``` -This method will first pull `amountSharesToPull` of SCY tokens, and use the floating amount of SCY tokens in the SCY contract to redeem to `tokenOut` (a **base token**). This pattern is similar to the one in `deposit` +This function will redeem the $d_s$ shares, which is equivalent to $d_a = d_s \times ExchangeRate(t)$ assets, from the pool. The $d_a$ assets is converted into exactly *amountTokenOut* of output token $i$ (*tokenOut*). + +If *burnFromInternalBalance* is set to *false*, the user will need to initially deposit *amountSharesToRedeem* into the SY contract, then this function will burn the floating amount $d_s$ of SY tokens (**shares**) in the SY contract to redeem to output token $i$ (*tokenOut*). This pattern is similar to UniswapV2 which allows for more gas efficient ways to interact with the contract. If *burnFromInternalBalance* is set to *true*, then this function will burn *amountSharesToRedeem* $d_s$ of SY tokens directly from the user to redeem to output token $i$ (*tokenOut*). -This function will redeem the exact SCY token (**shares**) from the SCY contract. The **assets** are converted into `tokenOut` of `tokenOut`. +This function should revert if $amountTokenOut \lt minTokenOut$. - **MUST** emit the `Redeem` event. - **MUST** support EIP-20’s `approve` / `transferFrom` flow where the shares are burned from receiver directly (as msg.sender) or if the msg.sender has EIP-20 approved allowance over the shares of the receiver. -- **MUST** revert if $tokenOut \lt minTokenOut$ (due to redeem limit being reached, slippage, or the user not approving enough `amountSharesToPull` **to the SCY contract, etc). +- **MUST** revert if $amountTokenOut \lt minTokenOut$ (due to redeem limit being reached, slippage, or the user not approving enough `amountSharesToRedeem` to the SY contract, etc). ```solidity -function claimRewards(address user) external returns (uint256[] memory rewardAmounts); +function exchangeRate() external view returns (uint256 res); ``` -This method sends all the available claimable rewards to the user as is, with the amounts in the list being in the same order as `getRewardTokens`. +This method updates and returns the latest **exchange rate**, which is the **exchange rate** from SY token amount into asset amount, scaled by a fixed scaling factor of 1e18. -- **MUST** emit the `ClaimRewards` event. -- **MAY** return one or multiple rewards to the user. -- **MAY** return zero rewards to the user. +- **MUST** return $ExchangeRate(t_{now})$ such that $ExchangeRate(t_{now}) \times syBalance / 1e18 = assetBalance$. +- **MUST NOT** include fees that are charged against the underlying yield token in the SY contract. ```solidity -function exchangeRateCurrent() external returns (uint256); +function getTokensIn() external view returns (address[] memory res); ``` -This method updates and returns the latest **exchange rate**, which is the **exchange rate** from SCY token amount into asset amount, scaled by a fixed scaling factor of 1e18. +This read-only method returns the list of all input tokens that can be used to deposit into the SY contract. -- **MUST** return $ExchangeRate(t_{now})$ such that $ExchangeRate(t_{now}) \times scyBalance / 1e18 = assetBalance$. -- **MUST NOT** include fees that are charged against the underlying yield token in the SCY contract. - -```solidity -function exchangeRateStored() external view returns (uint256); -``` - -This read-only method returns the last saved value of the exchange rate. - -- **MUST** return the value of `exchangeRateCurrent` of a past timestamp where it was last updated in the contract -- **MUST NOT** include fees that are charged against the underlying yield token in the SCY contract. -- **MUST NOT** revert. - -```solidity -function yieldToken() external view returns (address); -``` - -This read-only method returns the underlying yield-generating token (representing a GYGP) that was wrapped into a SCY token. - -- **MUST** return a token address that conforms to the EIP-20 interface, or zero address +- **MUST** return EIP-20 token addresses. +- **MUST** return at least one address. - **MUST NOT** revert. -- **MUST** reflect the exact underlying yield-generating token address if the SCY token is a wrapped token. -- **MAY** return 0x or zero address if the SCY token is natively implemented, and not from wrapping. ```solidity -function getRewardTokens() external view returns (address[] memory); +function getTokensOut() external view returns (address[] memory res); ``` -This read-only method returns the latest list of all reward tokens. +This read-only method returns the list of all output tokens that can be converted into when exiting the SY contract. - **MUST** return EIP-20 token addresses. +- **MUST** return at least one address. - **MUST NOT** revert. -- **MAY** return an empty list, one, or several token addresses. -- **MAY** return additional reward tokens over time, depending on when `underlyingYieldToken` supports more or less reward tokens. ```solidity -function getBaseTokens() external view returns (address[] memory); +function yieldToken() external view returns (address); ``` -This read-only method returns the list of all base tokens that can be used to deposit into the SCY contract. +This read-only method returns the underlying yield-bearing token (representing a GYGP) address. -- **MUST** return EIP-20 token addresses. -- **MUST** return at least one address. +- **MUST** return a token address that conforms to the EIP-20 interface, or zero address - **MUST NOT** revert. +- **MUST** reflect the exact underlying yield-bearing token address if the SY token is a wrapped token. +- **MAY** return 0x or zero address if the SY token is natively implemented, and not from wrapping. ```solidity -function isValidBaseToken(address token) external view returns (bool); +function previewDeposit(address tokenIn, uint256 amountTokenToDeposit) + external + view + returns (uint256 amountSharesOut); ``` -This read-only method checks whether a token address entered is a base token that can be used to mint SCY. +This read-only method returns the amount of shares that a user would have received if they deposit *amountTokenToDeposit* of *tokenIn*. +- **MUST** return less than or equal of *amountSharesOut* to the actual return value of the `deposit` method, and **SHOULD NOT** return greater than the actual return value of the `deposit` method. - **MUST NOT** revert. ```solidity -function assetInfo() +function previewRedeem(address tokenOut, uint256 amountSharesToRedeem) external view - returns ( - AssetType assetType, - address assetAddress, - uint8 assetDecimals - ); + returns (uint256 amountTokenOut); ``` -This read-only function returns useful information about the asset, intended for front-ends or off-chain systems to display balances and information about the asset. +This read-only method returns the amount of *tokenOut* that a user would have received if they redeem *amountSharesToRedeem* of *tokenOut*. -`decimals` is the decimals to format asset balances. - -Convention for `assetType` and format of the `info` field: 1) If asset is an EIP-20 token, `assetType = 0`, `assetAddress` is the address of the token; 2) If asset is liquidity of an AMM (like $\sqrt{k}$ in UniswapV2 forks), `assetType = 1`, `assetAddress` is the address of the LP token. - -* **MUST** reflect the underlying asset’s decimals if at all possible in order to eliminate any possible source of confusion or be deemed malicious. -- **MUST** conform to the conventions for assetType and info. +- **MUST** return less than or equal of *amountTokenOut* to the actual return value of the `redeem` method, and **SHOULD NOT** return greater than the actual return value of the `redeem` method. - **MUST NOT** revert. #### Events @@ -334,77 +305,50 @@ event Deposit( address indexed receiver, address indexed tokenIn, uint256 amountDeposited, - uint256 amountScyOut + uint256 amountSyOut ); ``` -`caller` has converted exact base tokens into SCY (shares) and transferred those SCY to `receiver`. +`caller` has converted exact *tokenIn* tokens into SY (shares) and transferred those SY to `receiver`. -- **MUST** be emitted when base tokens are deposited into the SCY contract via `deposit` method. +- **MUST** be emitted when input tokens are deposited into the SY contract via `deposit` method. ```solidity event Redeem( address indexed caller, address indexed receiver, address indexed tokenOut, - uint256 amountScyToRedeem, + uint256 amountSyToRedeem, uint256 amountTokenOut ); ``` -`caller` has converted exact SCY (shares) into base tokens and transferred those base tokens to `receiver`. - -- **MUST** be emitted when base tokens are redeemed from the SCY contract via `redeem` method. - -```solidity -event ClaimRewards( - address indexed caller, - address indexed user, - address[] rewardTokens, - uint256[] rewardAmounts -); -``` - -`caller` has claimed user rewards and transferred them to the user. +`caller` has converted exact SY (shares) into input tokens and transferred those input tokens to `receiver`. -- **MUST** be emitted when rewards are claimed from the SCY contract via `claimRewards` method. +- **MUST** be emitted when input tokens are redeemed from the SY contract via `redeem` method. -```solidity -event ExchangeRateUpdated(uint256 oldExchangeRate, uint256 newExchangeRate); -``` +**"SY" Word Choice:** -The `exchangeRateCurrent` is updated to the latest exchange rate. - -- **MUST** be emitted when the exchange rate is updated in the SCY contract via `exchangeRateCurrent` method. - -**"SCY" Word Choice:** - -"SCY" (pronunciation: */sʌɪ/*), an abbreviation of Super Composable Yield, was found to be appropriate to describe a broad universe of composable yield-bearing digital assets. +"SY" (pronunciation: */sʌɪ/*), an abbreviation of Standardized Yield, was found to be appropriate to describe a broad universe of standardized composable yield-bearing digital assets. ## Rationale -[EIP-20](./eip-20.md) is enforced because implementation details such as transfer, token approvals, and balance calculation directly carry over to the SCY tokens. This standardization makes the SCY tokens immediately compatible with all EIP-20 use cases. +[EIP-20](./eip-20.md) is enforced because implementation details such as transfer, token approvals, and balance calculation directly carry over to the SY tokens. This standardization makes the SY tokens immediately compatible with all EIP-20 use cases. -[EIP-165](./eip-165.md) is not explicitly mentioned to be supported as there are no optional methods in this standard. It is expected for all methods defined in the interface to be implemented. +[EIP-165](./eip-165.md) can optionally be implemented should you want integrations to detect the IStandardizedYield interface implementation. -[EIP-2612](./eip-2612.md) can optionally be implemented in order to improve the UX of approving SCY tokens on various integrations. - -The `exchangeRateStored` read-only method serves as a rough estimate of the prevalent exchange rate since the last update. It is included for frontends, wallets, and applications that need an estimate on the exchange rate of SCY tokens into assets, not an exact value possibly including slippage or other fees as this would require them doing a state update and spending gas. For applications that need an exact exchange rate, the `exchangeRateCurrent` mutable function can be used. +[EIP-2612](./eip-2612.md) can optionally be implemented in order to improve the UX of approving SY tokens on various integrations. ## Backwards Compatibility -This EIP is fully backwards compatible as its implementation extends the functionality of [EIP-20](./eip-20.md), however the optional metadata extensions, namely `name`, `decimals`, and `symbol` semantics MUST be implemented for all SCY token implementations. +This EIP is fully backwards compatible as its implementation extends the functionality of [EIP-20](./eip-20.md), however the optional metadata extensions, namely `name`, `decimals`, and `symbol` semantics MUST be implemented for all SY token implementations. ## Security Considerations Malicious implementations which conform to the interface can put users at risk. It is recommended that all integrators (such as wallets, aggregators, or other smart contract protocols) review the implementation to avoid possible exploits and users losing funds. -The method `exchangeRateStored` returns an outdated estimated value and does not confer the exact current exchange rate of asset per share. Should accuracy be needed, `exchangeRateCurrent` should be used instead (which additionally updates `exchangeRateStored`.) - -`decimals` in `assetInfo` must strongly reflect the underlying asset’s decimals if at all possible in order to eliminate any possible source of confusion or be deemed malicious. - -`yieldToken` must strongly reflect the address of the underlying wrapped yield-generating token. For a native implementation wherein the SCY token does not wrap a yield-generating token, but natively represents a GYGP share, then the address returned MAY be a zero address. Otherwise, for wrapped tokens, you may introduce confusion on what the SCY token represents, or may be deemed malicious. +`yieldToken` must strongly reflect the address of the underlying wrapped yield-bearing token. For a native implementation wherein the SY token does not wrap a yield-bearing token, but natively represents a GYGP share, then the address returned MAY be a zero address. Otherwise, for wrapped tokens, you may introduce confusion on what the SY token represents, or may be deemed malicious. ## 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). From d64f4c046b628cc78375c275ed5c2ab591c0503b Mon Sep 17 00:00:00 2001 From: Anton Buenavista Date: Thu, 22 Dec 2022 14:37:35 -0500 Subject: [PATCH 072/274] Update EIP-5115: Minor correction on interface (#6208) --- EIPS/eip-5115.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/EIPS/eip-5115.md b/EIPS/eip-5115.md index e86ef8a1a3c539..d93052455f2a34 100644 --- a/EIPS/eip-5115.md +++ b/EIPS/eip-5115.md @@ -127,11 +127,6 @@ On top of the definitions above for GYGPs, we need to define 2 more concepts: ```solidity interface IStandardizedYield { - enum AssetType { - TOKEN, - LIQUIDITY - } - event Deposit( address indexed caller, address indexed receiver, From 4edf0d10a324abee0417c3a8527d826e9811309d Mon Sep 17 00:00:00 2001 From: taek Date: Fri, 23 Dec 2022 09:03:56 +0900 Subject: [PATCH 073/274] Update eip-4337.md : typo desribed -> described (#6169) --- EIPS/eip-4337.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index bbfb8c01336497..474d9fae853805 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -38,7 +38,7 @@ This proposal takes a different approach, avoiding any adjustments to the consen * **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 + * unlike transaction, it contains several other fields, described 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. From 83beeb22ac47f2623103af8977cabfcb66908afa Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Fri, 23 Dec 2022 02:30:36 -0700 Subject: [PATCH 074/274] clarify that initcode which fails validation still pays 3860 initcode cost (#6210) --- EIPS/eip-3540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 05e40a0dd026bd..951492eaf5a37a 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -163,7 +163,7 @@ For clarity, the *container* refers to the complete account code, while *code* r 3. `PC` returns the current position within the *code*. 4. `CODECOPY`/`CODESIZE`/`EXTCODECOPY`/`EXTCODESIZE`/`EXTCODEHASH` keeps operating on the entire *container*. 5. The input to `CREATE`/`CREATE2` is still the entire *container*. -6. The size limit for deployed code as specified in [EIP-170](./eip-170.md) and for init code as specified in [EIP-3860](./eip-3860.md) is applied to the entire *container* size, not to the *code* size. +6. The size limit for deployed code as specified in [EIP-170](./eip-170.md) and for initcode as specified in [EIP-3860](./eip-3860.md) is applied to the entire *container* size, not to the *code* size. This also means if initcode validation fails, it is still charged the EIP-3860 `initcode_cost`. (*Remark:* Due to [EIP-4750](./eip-4750.md), `JUMP` and `JUMPI` are disabled and therefore are not discussed in relation to EOF.) From 4e5cd9fe6dd168165a7a5cfef68ead439ef27f17 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Fri, 23 Dec 2022 02:57:00 -0700 Subject: [PATCH 075/274] remove pc instruction from eof (#6209) --- EIPS/eip-4200.md | 2 +- EIPS/eip-4750.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index 828973d6a6a9e8..fc744bc6636c39 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -63,7 +63,7 @@ Because the destinations are validated upfront, the cost of these instructions a We chose relative addressing in order to support code which is relocatable. This also means a code snippet can be injected. A technique seen used prior to this EIP to achieve the same goal was to inject code like `PUSHn PC ADD JUMPI`. -We do not see any significant downside to relative addressing, but it also opens possibility to the deprecation of the `PC` instruction. +We do not see any significant downside to relative addressing and it allows us to also deprecate the `PC` instruction. ### Immediate size diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index e3e67a7674c160..87746d15aeb59c 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -106,10 +106,12 @@ In addition to container format validation rules above, we extend code section v ### Disallowed instructions -Dynamic jump instructions `JUMP` (`0x56`) and `JUMPI` (`0x57`) and invalid and their opcodes are undefined. +Dynamic jump instructions `JUMP` (`0x56`) and `JUMPI` (`0x57`) are invalid and their opcodes are undefined. `JUMPDEST` (`0x5b`) instruction is renamed to `NOP` ("no operation") without the change in behaviour: it pops nothing and pushes nothing to data stack and has no other effects except for `PC` increment and charging 1 gas. +`PC` (0x58) instruction becomes invalid and its opcode is undefined. + *Note:* This change implies that JUMPDEST analysis is no longer required for EOF code. ### Execution From af8847df2de8aac666aaf3fcabecf17827d4c1d4 Mon Sep 17 00:00:00 2001 From: Sujith Somraaj <35634175+sujithsomraaj@users.noreply.github.com> Date: Sun, 25 Dec 2022 22:34:36 +0530 Subject: [PATCH 076/274] Add EIP-6170: Cross-Chain Messaging Interface (#6172) * cross-chain messaging interface * fix: markdown linter fix * chore: updating EIP name * Apply suggestions from code review * chore: changing specification to natspec interface * Update eip-6170.md Co-authored-by: Matt Stam * Update eip-6170.md * Update eip-6170.md * chore: remove events to discuss further * Move file to correct folder Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Matt Stam --- EIPS/eip-6170.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 EIPS/eip-6170.md diff --git a/EIPS/eip-6170.md b/EIPS/eip-6170.md new file mode 100644 index 00000000000000..48339d5a2531b4 --- /dev/null +++ b/EIPS/eip-6170.md @@ -0,0 +1,79 @@ +--- +eip: 6170 +title: Cross-Chain Messaging Interface +description: A common smart contract interface for interacting with messaging protocols. +author: Sujith Somraaj (@sujithsomraaj) +discussions-to: https://ethereum-magicians.org/t/cross-chain-messaging-standard/12197 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-19 +--- + +## Abstract + +This EIP standardizes an interface for cross-chain messengers, providing basic functionality to send and receive a cross-chain message (state). + +## Motivation + +Cross-chain messaging protocols lack standardization, resulting in unnecessarily complex competing implementations: Layerzero, Hyperlane & Wormhole each use a different interface. This makes integration difficult at the aggregator or plugin layer for protocols that must conform to any standards and forces each protocol to implement its adapter. + +Even chain-native arbitrary messaging protocols like the MATIC State Tunnel has an application-specific interface. + +## 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. + +Every compliant messaging tunnels must implement the following interface. + +``` solidity +pragma solidity >=0.8.0; + +/// @title Cross-Chain Messaging interface +/// @dev Allows seamless interchain messaging. +/// @author Sujith Somraaj +/// Note: Bytes are used along the entire implementation to support non-evm chains. + +interface EIP6170 { + /// @dev Sends message to a receiving address on different blockchain. + /// @param chainId is the unique identifier of receiving blockchain. + /// @param receiver is the address of the receiver. + /// @param message is the arbitrary message to be delivered. + /// @return the status of the process on the sending chain. + /// Note: this function is designed to support both evm and non-evm chains + /// Note: proposing chain-ids be the bytes encoding of their native token name string. For eg., abi.encode("ETH"), abi.encode("SOL") imagining they cannot override. + function sendMessage( + bytes memory chainId, + bytes memory receiver, + bytes memory message + ) external returns (bool); + + /// @dev Receives message from a sender on different blockchain. + /// @param chainId is the unique identifier of the sending blockchain. + /// @param sender is the address of the sender. + /// @param message is the arbitrary message sent by the sender. + /// @return the status of message processing/storage. + /// Note: sender validation (or) message validation should happen before processing the message. + function receiveMessage( + bytes memory chainId, + bytes memory sender, + bytes memory message + ) external returns (bool); +} +``` + +## Rationale + +The Cross-Chain interface is designed to be optimized for interoperability layer integrators with a feature-complete, yet minimal interface. Validations such as sender authentication, receiver whitelisting, relayer mechanisms and cross-chain execution overrides are intentionally not specified, as Messaging protocols are expected to be treated as black boxes on-chain and inspected off-chain before use. + +## Security Considerations + +Fully permissionless messaging could be a security threat to the protocol. It is recommended that all the integrators review the implementation of messaging tunnels before integrating. + +For eg., without sender authentication, anyone could write arbitrary messages into the receiving smart contract. + +This EIP focuses only on the way the messages should be sent and received with a specific standard. But any authentication (or) message tunnel specific operations can be implemented inside the receive function by integrators. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md) From 63e9f33d5e4f6d180474f84ceb6f1ff761ffa9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Sun, 25 Dec 2022 18:26:55 +0100 Subject: [PATCH 077/274] Ignore JetBrains project files (#6211) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a8bc8dfb50d64d..8503a3cdadb0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ vendor # Editor files .gitpod.yml .DS_Store +/.idea # Secrets .vercel From 95381a532d075cac0a398e5edc678de28fd513dd Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:01:34 -0500 Subject: [PATCH 078/274] Update EIP-5568: Move to review (#6177) --- EIPS/eip-5568.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5568.md b/EIPS/eip-5568.md index dd5b21f852e697..6b18a057643051 100644 --- a/EIPS/eip-5568.md +++ b/EIPS/eip-5568.md @@ -4,7 +4,7 @@ title: Required Action Signals Using Revert Reasons description: Signal to wallets that an action is needed by returning a custom revert code author: Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/eip-5568-revert-signals/10622 -status: Draft +status: Review type: Standards Track category: Interface created: 2022-08-31 From f5d58e6b0e4d6f0038133670a9ec1f2e71c81cde Mon Sep 17 00:00:00 2001 From: Gaurang Torvekar Date: Sun, 25 Dec 2022 18:21:31 +0000 Subject: [PATCH 079/274] Update EIP-5606: Move to Last Call (#6151) * Update EIP-5606: Moving to Last Call * Update EIP-5606: Added Last Call Deadline * Update EIP-5606: Fixed a typo * Update EIPS/eip-5606.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-5606.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5606.md b/EIPS/eip-5606.md index 0cb79857928d7f..a4307f09e8d250 100644 --- a/EIPS/eip-5606.md +++ b/EIPS/eip-5606.md @@ -4,7 +4,8 @@ title: Multiverse NFTs description: A universal representation of multiple related NFTs as a single digital asset across various platforms author: Gaurang Torvekar (@gaurangtorvekar), Khemraj Adhawade (@akhemraj), Nikhil Asrani (@nikhilasrani) discussions-to: https://ethereum-magicians.org/t/eip-5606-multiverse-nfts-for-digital-asset-interoperability/10698 -status: Review +status: Last Call +last-call-deadline: 2023-01-02 type: Standards Track category: ERC created: 2022-09-06 From aa2ffec08d9e74ced887091cd88fb4a2fdd6fcc6 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sun, 25 Dec 2022 15:01:08 -0500 Subject: [PATCH 080/274] Update EIP-5219: Move to review (#6180) * Update EIP-5219: Move to review * Merge master into eip-5219-review (#6183) * Update EIP-5507: Add Rationale stub (#6181) * Update EIP-5219: Fix small typo (#6182) --- EIPS/eip-5219.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5219.md b/EIPS/eip-5219.md index 47036c23a98267..4521b6fb9052fa 100644 --- a/EIPS/eip-5219.md +++ b/EIPS/eip-5219.md @@ -4,7 +4,7 @@ title: Contract Resource Requests description: Allows the requesting of resources from contracts author: Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/pr-5219-discussion-contract-rest/9907 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-07-10 From 4943a9ec237e8445de5063e2b3bacc82a30023c4 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sun, 25 Dec 2022 15:26:08 -0500 Subject: [PATCH 081/274] Update EIP-5380: Second Revision (#6214) --- EIPS/eip-5380.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-5380.md b/EIPS/eip-5380.md index f4006789463342..32e996fc3e6c39 100644 --- a/EIPS/eip-5380.md +++ b/EIPS/eip-5380.md @@ -46,10 +46,11 @@ interface ERC721Entitlement is ERC165 { function maxEntitlements(address contract, uint256 tokenId) external view (uint256 max); /// @notice Get the user associated with the given contract and tokenId. - /// @dev Defaults to contract.ownerOf(tokenId) + /// @dev Defaults to maxEntitlements(contract, tokenId) assigned to contract.ownerOf(tokenId) + /// @param user The user to query /// @param contract The contract to query /// @param tokenId The tokenId to query - function entitlementOf(address contract, uint256 tokenId) external view returns (address user); + function entitlementOf(address user, address contract, uint256 tokenId) external view returns (uint256 amt); } ``` @@ -64,11 +65,11 @@ This OPTIONAL interface is RECOMMENDED. pragma solidity ^0.8.0; interface ERC721EntitlementEnumerable is ERC721Entitlement /* , ERC165 */ { - /// @notice Enumerate tokens assigned to a user + /// @notice Enumerate tokens with nonzero entitlement assigned to a user /// @dev Throws if the index is out of bounds or if user == address(0) /// @param user The user to query /// @param index A counter - function tokenOfUserByIndex(address user, uint256 index) external view returns (address contract, uint256 tokenId); + function entitlementOfUserByIndex(address user, uint256 index) external view returns (address contract, uint256 tokenId); } ``` @@ -84,7 +85,7 @@ No backward compatibility issues were found. ## Security Considerations -Needs discussion. +The security considerations of [EIP-721](./eip-721.md) apply. ## Copyright From 9c623d3edb4682f45ff48269a7e8b44a23ae0247 Mon Sep 17 00:00:00 2001 From: Peter Kohl-Landgraf <32535675+pekola@users.noreply.github.com> Date: Mon, 26 Dec 2022 00:23:42 +0100 Subject: [PATCH 082/274] Add EIP-6123: Smart Derivative Contract (#6123) * added eip markdown file, interface, reference implemntation, unit tests, readme and some docs * some minor corrections * some minor adjustment in file naming * improved markdown linter formatting rules * Assign EIP-6123 * Rename eip-0000.md to eip-6123.md * Apply suggestions from code review * Update assets/eip-0000/contracts/SDC.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update assets/eip-0000/contracts/ISDC.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update assets/eip-0000/contracts/SDCToken.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6123.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6123.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * - renamed asset-folder. added some more details in interface, added some aspects in reference implementation (inline) * - minor change in sample xml, updated forum link * - some corrections in interface doc, fixed typos in markdown * erc20 -> ERC-20 * ERC-20 -> erc-20 * erc-20 -> EIP-20 * Added markdown link. * Made Trade Data Spec a subsection. * Changed section title (singular). * Minor fix to grammar. * Updated reference to unit tests. Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Christian Fries --- EIPS/eip-6123.md | 236 +++++++++ assets/eip-6123/README.md | 47 ++ assets/eip-6123/contracts/ISDC.sol | 190 ++++++++ assets/eip-6123/contracts/SDC.sol | 446 ++++++++++++++++++ assets/eip-6123/contracts/SDCToken.sol | 15 + .../doc/sample-tradedata-filestructure.xml | 77 +++ .../doc/sdc_livecycle_sequence_diagram.png | Bin 0 -> 182042 bytes .../doc/sdc_trade_and_process_states.png | Bin 0 -> 82437 bytes assets/eip-6123/test/SDC.js | 128 +++++ 9 files changed, 1139 insertions(+) create mode 100644 EIPS/eip-6123.md create mode 100644 assets/eip-6123/README.md create mode 100644 assets/eip-6123/contracts/ISDC.sol create mode 100644 assets/eip-6123/contracts/SDC.sol create mode 100644 assets/eip-6123/contracts/SDCToken.sol create mode 100644 assets/eip-6123/doc/sample-tradedata-filestructure.xml create mode 100644 assets/eip-6123/doc/sdc_livecycle_sequence_diagram.png create mode 100644 assets/eip-6123/doc/sdc_trade_and_process_states.png create mode 100644 assets/eip-6123/test/SDC.js diff --git a/EIPS/eip-6123.md b/EIPS/eip-6123.md new file mode 100644 index 00000000000000..be7f54b0e883d9 --- /dev/null +++ b/EIPS/eip-6123.md @@ -0,0 +1,236 @@ +--- +eip: 6123 +title: Smart Derivative Contract +description: A deterministic protocol for frictionless post-trade processing of OTC financial contracts +author: Christian Fries (@cfries), Peter Kohl-Landgraf (@pekola), Alexandros Korpis (@kourouta) +discussions-to: https://ethereum-magicians.org/t/eip-6123-smart-derivative-contract-frictionless-processing-of-financial-derivatives/12134 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-13 +--- + +## Abstract + +The Smart Derivative Contract is a deterministic protocol to trade and process +financial derivative contracts frictionless and scalable in a completely automated way. Counterparty credit risk ís removed. +Known operational risks and complexities in post-trade processing are removed by construction as all process states +are fully specified and are known to the counterparties. + +## Motivation + +### Rethinking Financial Derivatives + +By their very nature, so-called "over-the-counter (OTC)" financial contracts are bilateral contractual agreements on the exchange of long-dated cash flow schedules. +Since these contracts change their intrinsic market value due to changing market environments, they are subject to counterparty credit risk when one counterparty is subject to default. +The initial white paper describes the concept of a Smart Derivative Contract with the central aim +to detach bilateral financial transactions from counterparty credit risk and to remove complexities +in bilateral post-trade processing by a complete redesign. + +### Concept of a Smart Derivative Contract + +A Smart Derivative Contract is a deterministic settlement protocol which has the same economic behaviour as a collateralized OTC +Derivative. Every process state is specified; therefore, the entire post-trade process is known in advance. +A Smart Derivative Contract (SDC) settles outstanding net present value of the underlying financial contract on a frequent basis. With each settlement cycle net present value of the underlying contract is +exchanged, and the value of the contract is reset to zero. Pre-Agreed margin buffers are locked at the beginning of each settlement cycle such that settlement will be guaranteed up to a certain amount. +If a counterparty fails to obey contract rules, e.g. not provide sufficient prefunding, SDC will terminate automatically with the guaranteed transfer of a termination fee by the causing party. +These features enable two counterparties to process their financial contract fully decentralized without relying on a third central intermediary agent. +The process logic of SDC can be implemented as a finite state machine on solidity. An [EIP-20](./eip-20.md) token can be used for frictionless decentralized settlement, see reference implementation. +Combined with an appropriate external market data and valuation oracle which calculates net present values, each known OTC derivative contract is able to be processed using this standard protocol. + + +## Specification + +### Methods + +The following methods specify inception and post-trade live cycle of a Smart Derivative Contract. For futher information also please look at the interface documentation ISDC.sol. + +#### inceptTrade + +A counterparty can initiate a trade by providing trade data as string and calling inceptTrade and initial settlement data. Only registered counteparties are allowed to use that function. + +```solidity +function inceptTrade(string memory _tradeData, string memory _initialSettlementData) external; +``` + +#### confirmTrade + +A counterparty can confirm a trade by providing the identical trade data and initial settlement information, which are already stored from inceptTrade call. + +```solidity +function confirmTrade(string memory _tradeData, string memory _initialSettlementData) external; +``` + +#### initiatePrefunding + +This method checks whether contractual prefunding is provided by both counterparties as agreed in the contract terms. Triggers a contract termination if not. + +```solidity +function initiatePrefunding() external; +``` + +#### initiateSettlement + +Allows eligible participants (such as counterparties or a delegated agent) to initiate a settlement. + +```solidity +function initiateSettlement() external; +``` + +#### performSettlement + +Valuation may be provided off-chain via an external oracle service that calculates net present value and uses external market data. +Method serves as callback called from an external oracle providing settlement amount and used settlement data which also get stored. +Settlement amount will be checked according to contract terms resulting in either a reqular settlement or a termination of the trade. + +```solidity +function performSettlement(int256 settlementAmount, string memory settlementData) external; +``` + +#### requestTermination + +Allows an eligible party to request a mutual termination + +```js +function requestTradeTermination(string memory tradeId) external; +``` + +#### confirmTradeTermination + +Allows eligible parties to confirm a formerly-requested mutual trade termination. + +```solidity +function confirmTradeTermination(string memory tradeId) external; +``` + +### Trade Events + +The following events are emitted during an SDC trade livecycle. + +#### TradeIncepted + +Emitted on trade inception - method 'inceptTrade' + +```solidity +event TradeIncepted(address initiator, string tradeId, string tradeData); +``` + +#### TradeConfirmed + +Emitted on trade confirmation - method 'confirmTrade' + +```solidity +event TradeConfirmed(address confirmer, string tradeId); +``` + +#### TradeActivated + +Emitted when trade is activated + +```solidity +event TradeActivated(string tradeId); +``` + +#### TradeTerminationRequest + +Emitted when termination request is initiated by a counterparty + +```solidity +event TradeTerminationRequest(address cpAddress, string tradeId); +``` + +#### TradeTerminationConfirmed + +Emitted when termination request is confirmed by a counterparty + +```solidity +event TradeTerminationConfirmed(address cpAddress, string tradeId); +``` + +#### TradeTerminated + +Emitted when trade is terminated + +```solidity +event TradeTerminated(string cause); +``` + +### Process Events + +The following events are emitted during SDC's process livecycle. + +#### ProcessAwaitingFunding + +Emitted when funding phase is initiated + +```solidity +event ProcessAwaitingFunding(); +``` + +#### ProcessFunded + +Emitted when funding has completed successfully - method 'initiatePrefunding' + +```solidity +event ProcessFunded(); +``` + +#### ProcessSettlementRequest + +Emitted when a settlement is initiated - method 'initiateSettlement' + +```solidity +event ProcessSettlementRequest(string tradeData, string lastSettlementData); +``` + +#### ProcessSettled + +Emitted when settlement was processed successfully - method 'performSettlement' + +```solidity +event ProcessSettled(); +``` + +## Rationale + +The interface design and reference implementation are based on the following considerations: + +- A SDC protocol is supposed to be used by two counterparties and enables them to initiate and process a derivative transaction in a bilateral and digital manner. +- The provided interface specification is supposed to completely reflect the entire trade livecycle. +- The interface specification is generic enough to handle the case that two counterparties process one or even multiple derivative transactions (on a netted base) +- Usually, the valuation of an OTC trade will require advanced valuation methodology. This is why the concept will in most cases rely on external market data and valuation algorithms +- A pull-based valuation based oracle pattern is specified by a simple callback pattern (methods: initiateSettlement, performSettlement) +- The reference implementation `SDC.sol` is based on a state-machine pattern where the states also serve as guards (via modifiers) to check which method is allowed to be called at a particular given process and trade state +- Java based state machine and contract implementations are also available. See the github repo link below. + +### State diagram of trade and process states + +![image info](../assets/eip-6123/doc/sdc_trade_and_process_states.png) + +### Sequence diagram of trade initiation and settlement livecycle + +![image info](../assets/eip-6123/doc/sdc_livecycle_sequence_diagram.png) + +## Test Cases + +Live-cycle unit tests based on the sample implementation and usage of [EIP-20](./eip-20.md) token is provided. See file [test/SDC.js](../assets/eip-6123/test/SDC.js) +). + +## Reference Implementation + +A reference implementation SDC.sol is provided and is based on the [EIP-20](./eip-20.md) token standard. +See folder /assets/contracts, more explanation on the implementation is provided inline. + +### Trade Data Specification (suggestion) + +Please take a look at the provided xml file as a suggestion on how trade parameters could be stored. + +## Security Considerations + +No known security issues up to now. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). + + diff --git a/assets/eip-6123/README.md b/assets/eip-6123/README.md new file mode 100644 index 00000000000000..2bf456a68bda04 --- /dev/null +++ b/assets/eip-6123/README.md @@ -0,0 +1,47 @@ +# SDC Solidity implementation + +## Description +This sdc implementation aims to implement process logic in a very lean way using an integrative solidity implementation and according unit tests + +### Provided Contracts and Tests +- `contracts/ISDC.sol` - Interface contract +- `contracts/SDC.sol` - SDC reference implementation contract +- `contracts/SDCToken.sol` - Mintable token contract for unit tests +- `test/SDC.js` - Unit tests for livecycle of sdc implementation + +### Used javascript based testing libraries for solidity +- `ethereum-waffle`: Waffle is a Solidity testing library. It allows you to write tests for your contracts with JavaScript. +- `chai`: Chai is an assertion library and provides functions like expect. +- `ethers`: This is a popular Ethereum client library. It allows you to interface with blockchains that implement the Ethereum API. +- `solidity-coverage`: This library gives you coverage reports on unit tests with the help of Istanbul. + +### Compile and run tests with hardhat +We provide the essential steps to compile the contracts and run provided unit tests +Check that you have the latest version of npm and node via `npm -version` (should be better than 8.5.0) and `node -v` (should be better than 16.14.2). + +1. Check out project +2. Go to folder and initialise a new npm project: `npm init -y`. A basic `package.json` file should occur +3. Install Hardhat as local solidity dev environment: `npx hardhat` +4. Select following option: Create an empty hardhat.config.js +5. Install Hardhat as a development dependency: `npm install --save-dev hardhat` +6. Install further testing dependencies: +`npm install --save-dev @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers ethereum-waffle chai ethers solidity-coverage` +7. Install open zeppelin contracts: `npm install @openzeppelin/contracts` +8. add plugins to hardhat.config.ts: +``` +require("@nomiclabs/hardhat-waffle"); +require('solidity-coverage'); +``` + +9. Adding commands to `package.json`: +``` +"scripts": { + "build": "hardhat compile", + "test:light": "hardhat test", + "test": "hardhat coverage" + }, +``` +9. run `npm run build` +10. run `npm run test` + + diff --git a/assets/eip-6123/contracts/ISDC.sol b/assets/eip-6123/contracts/ISDC.sol new file mode 100644 index 00000000000000..3353ec4e68b04a --- /dev/null +++ b/assets/eip-6123/contracts/ISDC.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity >=0.7.0 <0.9.0; + +/*------------------------------------------- DESCRIPTION ---------------------------------------------------------------------------------------*/ + +/** + * @title ERC6123 Smart Derivative Contract + * @dev Interface specification for a Smart Derivative Contract, which specifies the post-trade live cycle of an OTC financial derivative in a completely deterministic way. + * Counterparty Risk is removed by construction. + * + * A Smart Derivative Contract is a deterministic settlement protocol which has economically the same behaviour as a collateralized OTC financial derivative. + * It aims is to remove many inefficiencies in collateralized OTC transactions and remove counterparty credit risk by construction. + * + * In contrast to a collateralized derivative contract based and collateral flows are netted. As result, the smart derivative contract generates a stream of + * reflecting the settlement of a referenced underlying. The settlement cash flows may be daily (which is the standard frequency in traditional markets) + * or at higher frequencies. + * With each settlement flow the change is the (discounting adjusted) net present value of the underlying contract is exchanged and the value of the contract is reset to zero. + * + * To automatically process settlement, counterparties need to provide sufficient prefunded margin amounts and termination fees at the + * beginning of each settlement cycle. Through a settlement cycle the margin amounts are locked. Simplified, the contract reverts the classical scheme of + * 1) underlying valuation, then 2) funding of a margin call to + * 1) pre-funding of a margin buffer (a token), then 2) settlement. + * + * A SDC automatically terminates the derivatives contract if there is insufficient pre-funding or if the settlement amount exceeds a + * prefunded margin balance. Beyond mutual termination is also intended by the function specification. + * + * Events and Functionality specify the entire live cycle: TradeInception, TradeConfirmation, TradeTermination, Margin-Account-Mechanics, Valuation and Settlement. + * + * The process can be described by time points and time-intervals which are associated with well defined states: + *

    + *
  1. t < T* (befrore incept). + *
  2. + *
  3. + * The process runs in cycles. Let i = 0,1,2,... denote the index of the cycle. Within each cycle there are times + * T_{i,0}, T_{i,1}, T_{i,2}, T_{i,3} with T_{i,1} = pre-funding of the Smart Contract, T_{i,2} = request valuation from oracle, T_{i,3} = perform settlement on given valuation, T_{i+1,0} = T_{i,3}. + *
  4. + *
  5. + * Given this time discretization the states are assigned to time points and time intervalls: + *
    + *
    Idle
    + *
    Before incept or after terminate
    + * + *
    Initiation
    + *
    T* < t < T_{0}, where T* is time of incept and T_{0} = T_{0,0}
    + * + *
    AwaitingFunding
    + *
    T_{i,0} < t < T_{i,1}
    + * + *
    Funding
    + *
    t = T_{i,1}
    + * + *
    AwaitingSettlement
    + *
    T_{i,1} < t < T_{i,2}
    + * + *
    ValuationAndSettlement
    + *
    T_{i,2} < t < T_{i,3}
    + * + *
    Settled
    + *
    t = T_{i,3}
    + *
    + *
  6. + *
+ */ + +interface ISDC { + /*------------------------------------------- EVENTS ---------------------------------------------------------------------------------------*/ + /** + * @dev Emitted when a new trade is incepted from a eligible counterparty + * @param initiator is the address from which trade was incepted + * @param tradeID is the tradeID (e.g. generated internally) + * @param tradeData holding the trade parameters + */ + event TradeIncepted(address initiator, string tradeId, string tradeData); + + /** + * @dev Emitted when an incepted trade is confirmed by the opposite counterparty + * @param confirmer the confirming party + * @param tradeId the trade identifier + */ + event TradeConfirmed(address confirmer, string tradeId); + + /** + * @dev Emitted when a confirmed trade is set to active - e.g. when termination fee amounts are provided + * @param tradeId the trade identifier of the activated trade + */ + event TradeActivated(string tradeId); + + /** + * @dev Emitted when an active trade is terminated + * @param cause string holding the cause of the termination + */ + event TradeTerminated(string cause); + + /** + * @dev Emitted when funding phase is initiated + */ + event ProcessAwaitingFunding(); + + /** + * @dev Emitted when margin balance was updated and sufficient funding is provided + */ + event ProcessFunded(); + + /** + * @dev Emitted when a valuation and settlement is requested + * @param tradeData holding the stored trade data + * @param lastSettlementData holding the settlementdata from previous settlement (next settlement will be the increment of next valuation compared to former valuation) + */ + event ProcessSettlementRequest(string tradeData, string lastSettlementData); + + /** + * @dev Emitted when a settlement was processed succesfully + */ + event ProcessSettled(); + + /** + * @dev Emitted when a counterparty proactively requests an early termination of the underlying trade + * @param cpAddress the address of the requesting party + * @param tradeID the trade identifier which is supposed to be terminated + */ + event TradeTerminationRequest(address cpAddress, string tradeId); + + /** + * @dev Emitted when early termination request is confirmed by the opposite party + * @param cpAddress the party which confirms the trade termination + * @param tradeID the trade identifier which is supposed to be terminated + */ + event TradeTerminationConfirmed(address cpAddress, string tradeId); + + /*------------------------------------------- FUNCTIONALITY ---------------------------------------------------------------------------------------*/ + + /// Trade Inception + + /** + * @notice Handles trade inception, stores trade data + * @dev emits a {TradeIncepted} event + * @param _tradeData a description of the trade specification e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) + */ + function inceptTrade(string memory _tradeData, string memory _initialSettlementData) external; + + /** + * @notice Performs a matching of provided trade data and settlement data + * @dev emits a {TradeConfirmed} event if trade data match + * @param _tradeData a description of the trade in sdc.xml, e.g. in xml format, suggested structure - see assets/eip-6123/doc/sample-tradedata-filestructure.xml + * @param _initialSettlementData the initial settlement data (e.g. initial market data at which trade was incepted) + */ + function confirmTrade(string memory _tradeData, string memory _initialSettlementData) external; + + /// Settlement Cycle: Prefunding + + /** + * @notice Called from outside to check and secure pre-funding. Terminate the trade if prefunding fails. + * @dev emits a {ProcessFunded} event if prefunding check is successful or a {TradeTerminated} event if prefunding check fails + */ + function initiatePrefunding() external; + + /// Settlement Cycle: Settlement + + /** + * @notice Called to trigger a (maybe external) valuation of the underlying contract and afterwards the according settlement process + * @dev emits a {ProcessSettlementRequest} + */ + function initiateSettlement() external; + + /** + * @notice Called from outside to trigger according settlement on chain-balances callback for initiateSettlement() event handler + * @dev emits a {ProcessSettled} if settlement is successful or {TradeTerminated} if settlement fails + * @param settlementAmount the settlement amount. If settlementAmount > 0 then receivingParty receives this amount from other party. If settlementAmount < 0 then other party receives -settlementAmount from receivingParty. + * @param settlementData. the tripple (product, previousSettlementData, settlementData) determines the settlementAmount. + */ + function performSettlement(int256 settlementAmount, string memory settlementData) external; + + /// Trade termination + + /** + * @notice Called from a counterparty to request a mutual termination + * @dev emits a {TradeTerminationRequest} + * @param tradeID the trade identifier which is supposed to be terminated + */ + function requestTradeTermination(string memory tradeId) external; + + /** + * @notice Called from a counterparty to confirm a termination, which will triggers a final settlement before trade gets inactive + * @dev emits a {TradeTerminationConfirmed} + * @param tradeID the trade identifier of the trade which is supposed to be terminated + */ + function confirmTradeTermination(string memory tradeId) external; +} + diff --git a/assets/eip-6123/contracts/SDC.sol b/assets/eip-6123/contracts/SDC.sol new file mode 100644 index 00000000000000..9c1cf84f4faea5 --- /dev/null +++ b/assets/eip-6123/contracts/SDC.sol @@ -0,0 +1,446 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity >=0.8.0 <0.9.0; + +import "./ISDC.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + + +/** + * @title Reference Implementation of ERC6123 - Smart Derivative Contract + * @notice This reference implementation is based on a finite state machine with predefined trade and process states (see enums below) + * Some comments on the implementation: + * - trade and process states are used in modifiers to check which function is able to be called at which state + * - trade data are stored in the contract + * - trade data matching is done in incept and confirm routine (comparing the hash of the provided data) + * - ERC-20 token is used for three participants: counterparty1 and counterparty2 and sdc + * - when prefunding is done sdc contract will hold agreed amounts and perform settlement on those + * - sdc also keeps track on internal balances for each counterparty + * - during prefunding sdc will transfer required amounts to its own balance - therefore sufficient approval is needed + * - upon termination all remaining 'locked' amounts will be transferred back to the counterparties +*/ + +contract SDC is ISDC { + /* + * Trade States + */ + enum TradeState { + + /* + * State before the trade is incepted. + */ + Inactive, + + /* + * Incepted: Trade data submitted by one party. Market data for initial valuation is set. + */ + Incepted, + + /* + * Confirmed: Trade data accepted by other party. + */ + Confirmed, + + /* + * Active (Confirmend + Prefunded Termination Fees). Will cycle through process states. + */ + Active, + + /* + * Terminated. + */ + Terminated + } + + /* + * Process States. t < T* (vor incept). The process runs in cycles. Let i = 0,1,2,... denote the index of the cycle. Within each cycle there are times + * T_{i,0}, T_{i,1}, T_{i,2}, T_{i,3} with T_{i,1} = pre-funding of the Smart Contract, T_{i,2} = request valuation from oracle, T_{i,3} = perform settlement on given valuation, T_{i+1,0} = T_{i,3}. + * Given this time discretization the states are assigned to time points and time intervalls: + * Idle: Before incept or after terminate + * Initiation: T* < t < T_{0}, where T* is time of incept and T_{0} = T_{0,0} + * AwaitingFunding: T_{i,0} < t < T_{i,1} + * Funding: t = T_{i,1} + * AwaitingSettlement: T_{i,1} < t < T_{i,2} + * ValuationAndSettlement: T_{i,2} < t < T_{i,3} + * Settled: t = T_{i,3} + */ + enum ProcessState { + /** + * @dev The process has not yet started or is terminated + */ + Idle, + /* + * @dev The process is initiated (incepted, but not yet completed confimation). Next: AwaitingFunding + */ + Initiation, + /* + * @dev Awaiiting preparation for funding the smart contract. Next: Funding + */ + AwaitingFunding, + /* + * @dev Prefunding the smart contract. Next: AwaitingSettlement + */ + Funding, + /* + * @dev The smart contract is completely funded and awaits settlement. Next: ValuationAndSettlement + */ + Funded, + /* + * @dev The settlement process is initiated. Next: Settled or InTermination + */ + ValuationAndSettlement, + /* + * @dev Termination started. + */ + InTermination + } + + struct MarginRequirement { + int256 buffer; + int256 terminationFee; + } + + /* + * Modifiers serve as guards whether at a specific process state a specific function can be called + */ + + modifier onlyCounterparty() { + require(msg.sender == party1 || msg.sender == party2, "You are not a counterparty."); _; + } + modifier onlyWhenTradeInactive() { + require(tradeState == TradeState.Inactive, "Trade state is not 'Inactive'."); _; + } + modifier onlyWhenTradeIncepted() { + require(tradeState == TradeState.Incepted, "Trade state is not 'Incepted'."); _; + } + modifier onlyWhenProcessAwaitingFunding() { + require(processState == ProcessState.AwaitingFunding, "Process state is not 'AwaitingFunding'."); _; + } + modifier onlyWhenProcessFundedAndTradeActive() { + require(processState == ProcessState.Funded && tradeState == TradeState.Active, "Process state is not 'Funded' or Trade is not 'Active'."); _; + } + modifier onlyWhenProcessValuationAndSettlement() { + require(processState == ProcessState.ValuationAndSettlement, "Process state is not 'ValuationAndSettlement'."); _; + } + TradeState private tradeState; + ProcessState private processState; + + address public party1; + address public party2; + address private immutable receivingPartyAddress; // Determine the receiver: Positive values are consider to be received by receivingPartyAddress. Negative values are received by the other counterparty. + + /* + * liquidityToken holds: + * - funding account of party1 + * - funding account of party2 + * - account for SDC (sum - this is split among parties by sdcBalances) + */ + IERC20 private liquidityToken; + + string private tradeID; + string private tradeData; + string private lastSettlementData; + + mapping(address => MarginRequirement) private marginRequirements; // Storage of M and P per counterparty address + mapping(uint256 => address) private pendingRequests; // Stores open request hashes for several requests: initiation, update and termination + + mapping(address => int256) private sdcBalances; // internal book-keeping: needed to track what part of the gross token balance is held for each party + + + bool private mutuallyTerminated = false; + + constructor( + address counterparty1, + address counterparty2, + address receivingParty, + address tokenAddress, + uint256 initialMarginRequirement, + uint256 initalTerminationFee + ) { + party1 = counterparty1; + party2 = counterparty2; + receivingPartyAddress = receivingParty; + liquidityToken = IERC20(tokenAddress); + tradeState = TradeState.Inactive; + processState = ProcessState.Idle; + marginRequirements[party1] = MarginRequirement(int256(initialMarginRequirement), int256(initalTerminationFee)); + marginRequirements[party2] = MarginRequirement(int256(initialMarginRequirement), int256(initalTerminationFee)); + sdcBalances[party1] = 0; + sdcBalances[party2] = 0; + } + + /* + * generates a hash from tradeData and generates a map entry in openRequests + * emits a TradeIncepted + * can be called only when TradeState = Incepted + */ + function inceptTrade(string memory _tradeData, string memory _initialSettlementData) external override onlyCounterparty onlyWhenTradeInactive + { + processState = ProcessState.Initiation; + tradeState = TradeState.Incepted; // Set TradeState to Incepted + + uint256 hash = uint256(keccak256(abi.encode(_tradeData, _initialSettlementData))); + pendingRequests[hash] = msg.sender; + tradeID = Strings.toString(hash); + tradeData = _tradeData; // Set Trade Data to enable querying already in inception state + + emit TradeIncepted(msg.sender, tradeID, _tradeData); + } + + /* + * generates a hash from tradeData and checks whether an open request can be found by the opposite party + * if so, data are stored and open request is deleted + * emits a TradeConfirmed + * can be called only when TradeState = Incepted + */ + function confirmTrade(string memory _tradeData, string memory _initialSettlementData) external override onlyCounterparty onlyWhenTradeIncepted + { + address pendingRequestParty = msg.sender == party1 ? party2 : party1; + uint256 tradeIDConf = uint256(keccak256(abi.encode(_tradeData, _initialSettlementData))); + require(pendingRequests[tradeIDConf] == pendingRequestParty, "Confirmation fails due to inconsistent trade data or wrong party address"); + delete pendingRequests[tradeIDConf]; // Delete Pending Request + + tradeState = TradeState.Confirmed; + emit TradeConfirmed(msg.sender, tradeID); + + // Pre-Conditions + if(_lockTerminationFees()) { + tradeState = TradeState.Active; + emit TradeActivated(tradeID); + + processState = ProcessState.AwaitingFunding; + emit ProcessAwaitingFunding(); + } + } + + /** + * Check sufficient balances and lock Termination Fees otherwise trade does not get activated + */ + function _lockTerminationFees() internal returns(bool) { + bool isAvailableParty1 = (liquidityToken.balanceOf(party1) >= uint(marginRequirements[party1].terminationFee)) && (liquidityToken.allowance(party1,address(this)) >= uint(marginRequirements[party1].terminationFee)); + bool isAvailableParty2 = (liquidityToken.balanceOf(party2) >= uint(marginRequirements[party2].terminationFee)) && (liquidityToken.allowance(party2,address(this)) >= uint(marginRequirements[party2].terminationFee)); + if (isAvailableParty1 && isAvailableParty2){ + liquidityToken.transferFrom(party1, address(this), uint(marginRequirements[party1].terminationFee)); // transfer termination fee party1 to sdc + liquidityToken.transferFrom(party2, address(this), uint(marginRequirements[party2].terminationFee)); // transfer termination fee party2 to sdc + adjustSDCBalances(marginRequirements[party1].terminationFee, marginRequirements[party2].terminationFee); // Update internal balances + return true; + } + else{ + tradeState == TradeState.Inactive; + processState = ProcessState.Idle; + emit TradeTerminated("Termination Fee could not be locked."); + return false; + } + } + + /* + * Failsafe: Free up accounts upon termination + */ + function _processTermination() internal { + liquidityToken.transfer(party1, uint256(sdcBalances[party1])); + liquidityToken.transfer(party2, uint256(sdcBalances[party2])); + + processState = ProcessState.Idle; + tradeState = TradeState.Inactive; + } + + /* + * Settlement Cycle + */ + + /* + * Send an Lock Request Event only when Process State = Funding + * Puts Process state to Margin Account Check + * can be called only when ProcessState = AwaitingFunding + */ + function initiatePrefunding() external override onlyWhenProcessAwaitingFunding { + processState = ProcessState.Funding; + + uint256 balanceParty1 = liquidityToken.balanceOf(party1); + uint256 balanceParty2 = liquidityToken.balanceOf(party2); + + /* Calculate gap amount for each party, i.e. residual between buffer and termination fee and actual balance */ + // max(M+P - sdcBalance,0) + uint gapAmountParty1 = marginRequirements[party1].buffer + marginRequirements[party1].terminationFee - sdcBalances[party1] > 0 ? uint(marginRequirements[party1].buffer + marginRequirements[party1].terminationFee - sdcBalances[party1]) : 0; + uint gapAmountParty2 = marginRequirements[party2].buffer + marginRequirements[party2].terminationFee - sdcBalances[party2] > 0 ? uint(marginRequirements[party2].buffer + marginRequirements[party2].terminationFee - sdcBalances[party2]) : 0; + + /* Good case: Balances are sufficient and token has enough approval */ + if ( (balanceParty1 >= gapAmountParty1 && liquidityToken.allowance(party1,address(this)) >= gapAmountParty1) && + (balanceParty2 >= gapAmountParty2 && liquidityToken.allowance(party2,address(this)) >= gapAmountParty2) ) { + liquidityToken.transferFrom(party1, address(this), gapAmountParty1); // Transfer of GapAmount to sdc contract + liquidityToken.transferFrom(party2, address(this), gapAmountParty2); // Transfer of GapAmount to sdc contract + processState = ProcessState.Funded; + adjustSDCBalances(int(gapAmountParty1),int(gapAmountParty2)); // Update internal balances + emit ProcessFunded(); + } + /* Party 1 - Bad case: Balances are insufficient or token has not enough approval */ + else if ( (balanceParty1 < gapAmountParty1 || liquidityToken.allowance(party1,address(this)) < gapAmountParty1) && + (balanceParty2 >= gapAmountParty2 && liquidityToken.allowance(party2,address(this)) >= gapAmountParty2) ) { + tradeState = TradeState.Terminated; + processState = ProcessState.InTermination; + + adjustSDCBalances(-marginRequirements[party1].terminationFee,marginRequirements[party1].terminationFee); // Update internal balances + + _processTermination(); // Release all buffers + emit TradeTerminated("Termination caused by party1 due to insufficient prefunding"); + } + /* Party 2 - Bad case: Balances are insufficient or token has not enough approval */ + else if ( (balanceParty1 >= gapAmountParty1 && liquidityToken.allowance(party1,address(this)) >= gapAmountParty1) && + (balanceParty2 < gapAmountParty2 || liquidityToken.allowance(party2,address(this)) < gapAmountParty2) ) { + tradeState = TradeState.Terminated; + processState = ProcessState.InTermination; + + adjustSDCBalances(marginRequirements[party2].terminationFee,-marginRequirements[party2].terminationFee); // Update internal balances + + _processTermination(); // Release all buffers + emit TradeTerminated("Termination caused by party2 due to insufficient prefunding"); + } + /* Both parties fail: Cross Transfer of Termination Fee */ + else { + tradeState = TradeState.Terminated; + processState = ProcessState.InTermination; + // if ( (balanceParty1 < gapAmountParty1 || liquidityToken.allowance(party1,address(this)) < gapAmountParty1) && (balanceParty2 < gapAmountParty2 || liquidityToken.allowance(party2,address(this)) < gapAmountParty2) ) { tradeState = TradeState.Terminated; + adjustSDCBalances(marginRequirements[party2].terminationFee-marginRequirements[party1].terminationFee,marginRequirements[party1].terminationFee-marginRequirements[party2].terminationFee); // Update internal balances: Cross Booking of termination fee + + _processTermination(); // Release all buffers + emit TradeTerminated("Termination caused by both parties due to insufficient prefunding"); + } + } + + /* + * Settlement can be initiated when margin accounts are locked, a valuation request event is emitted containing tradeData and valuationViewParty + * Changes Process State to Valuation&Settlement + * can be called only when ProcessState = Funded and TradeState = Active + */ + function initiateSettlement() external override onlyCounterparty onlyWhenProcessFundedAndTradeActive + { + processState = ProcessState.ValuationAndSettlement; + emit ProcessSettlementRequest(tradeData, lastSettlementData); + } + + /* + * Performs a settelement only when processState is ValuationAndSettlement + * Puts process state to "inTransfer" + * Checks Settlement amount according to valuationViewParty: If SettlementAmount is > 0, valuationViewParty receives + * can be called only when ProcessState = ValuationAndSettlement + */ + function performSettlement(int256 settlementAmount, string memory settlementData) onlyWhenProcessValuationAndSettlement external override + { + lastSettlementData = settlementData; + address receivingParty = settlementAmount > 0 ? receivingPartyAddress : other(receivingPartyAddress); + address payingParty = other(receivingParty); + + bool noTermination = abs(settlementAmount) <= marginRequirements[payingParty].buffer; + int256 transferAmount = (noTermination == true) ? abs(settlementAmount) : marginRequirements[payingParty].buffer + marginRequirements[payingParty].terminationFee; // Override with Buffer and Termination Fee: Max Transfer + + if(receivingParty == party1) // Adjust internal Balances, only debit is booked on sdc balance as receiving party obtains transfer amount directly from sdc + adjustSDCBalances(0, -transferAmount); + else + adjustSDCBalances(-transferAmount, 0); + + liquidityToken.transfer(receivingParty, uint256(transferAmount)); // SDC contract performs transfer to receiving party + + if (noTermination) { // Regular Settlement + emit ProcessSettled(); + processState = ProcessState.AwaitingFunding; // Set ProcessState to 'AwaitingFunding' + } else { // Termination Event, buffer not sufficient, transfer margin buffer and termination fee and process termination + tradeState = TradeState.Terminated; + processState = ProcessState.InTermination; + _processTermination(); // Transfer all locked amounts + emit TradeTerminated("Termination due to margin buffer exceedance"); + } + + if (mutuallyTerminated) { // Both counterparties agreed on a premature termination + processState = ProcessState.InTermination; + _processTermination(); + } + } + + /* + * End of Cycle + */ + + /* + * Can be called by a party for mutual termination + * Hash is generated an entry is put into pendingRequests + * TerminationRequest is emitted + * can be called only when ProcessState = Funded and TradeState = Active + */ + function requestTradeTermination(string memory _tradeID) external override onlyCounterparty onlyWhenProcessFundedAndTradeActive + { + require(keccak256(abi.encodePacked(tradeID)) == keccak256(abi.encodePacked(_tradeID)), "Trade ID mismatch"); + uint256 hash = uint256(keccak256(abi.encode(_tradeID, "terminate"))); + pendingRequests[hash] = msg.sender; + emit TradeTerminationRequest(msg.sender, _tradeID); + } + + /* + + * Same pattern as for initiation + * confirming party generates same hash, looks into pendingRequests, if entry is found with correct address, tradeState is put to terminated + * can be called only when ProcessState = Funded and TradeState = Active + */ + function confirmTradeTermination(string memory tradeId) external override onlyCounterparty onlyWhenProcessFundedAndTradeActive + { + address pendingRequestParty = msg.sender == party1 ? party2 : party1; + uint256 hashConfirm = uint256(keccak256(abi.encode(tradeId, "terminate"))); + require(pendingRequests[hashConfirm] == pendingRequestParty, "Confirmation of termination failed due to wrong party or missing request"); + delete pendingRequests[hashConfirm]; + mutuallyTerminated = true; + emit TradeTerminationConfirmed(msg.sender, tradeID); + } + + function adjustSDCBalances(int256 adjustmentAmountParty1, int256 adjustmentAmountParty2) internal { + if (adjustmentAmountParty1 < 0) + require(sdcBalances[party1] >= adjustmentAmountParty1, "SDC Balance Adjustment fails for Party1"); + if (adjustmentAmountParty2 < 0) + require(sdcBalances[party2] >= adjustmentAmountParty2, "SDC Balance Adjustment fails for Party2"); + sdcBalances[party1] = sdcBalances[party1] + adjustmentAmountParty1; + sdcBalances[party2] = sdcBalances[party2] + adjustmentAmountParty2; + } + + /* + * Utilities + */ + + /** + * Absolute value of an integer + */ + function abs(int x) private pure returns (int) { + return x >= 0 ? x : -x; + } + + /** + * Other party + */ + function other(address party) private view returns (address) { + return (party == party1 ? party2 : party1); + } + + function getTokenAddress() public view returns(address) { + return address(liquidityToken); + } + + function getTradeID() public view returns (string memory) { + return tradeID; + } + + function getTradeData() public view returns (string memory) { + return tradeData; + } + + + function getTradeState() public view returns (TradeState) { + return tradeState; + } + + function getProcessState() public view returns (ProcessState) { + return processState; + } + + function getOwnSdcBalance() public view returns (int256) { + return sdcBalances[msg.sender]; + } + + /**END OF FUNCTIONS WHICH ARE ONLY USED FOR TESTING PURPOSES */ +} \ No newline at end of file diff --git a/assets/eip-6123/contracts/SDCToken.sol b/assets/eip-6123/contracts/SDCToken.sol new file mode 100644 index 00000000000000..45de674b998580 --- /dev/null +++ b/assets/eip-6123/contracts/SDCToken.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SDCToken is ERC20{ + constructor() ERC20("SDCToken", "SDCT"){ + + } + + function mint(address to, uint256 amount) public{ + _mint(to,amount); + } +} \ No newline at end of file diff --git a/assets/eip-6123/doc/sample-tradedata-filestructure.xml b/assets/eip-6123/doc/sample-tradedata-filestructure.xml new file mode 100644 index 00000000000000..c557553cb293c8 --- /dev/null +++ b/assets/eip-6123/doc/sample-tradedata-filestructure.xml @@ -0,0 +1,77 @@ + + + + net.finmath + finmath-smart-derivative-contract + 0.1.8 + + + + + Counterparty 1 + party1 + + constant + 10000.0 + + + constant + 1000.0 + +
0x...
+
+ + Counterparty 2 + party2 + + constant + 10000.0 + + + constant + 1000.0 + +
0x...
+
+
+ + + 2011-12-13T10:15:30 + + + daily + 17:00 + + + xyz + + symbol1 + symbol2 + ... + + + + + party1 + + + + + + + + + CP1 + + + + CP2 + + 2022-12-13 + + + + + + +
\ No newline at end of file diff --git a/assets/eip-6123/doc/sdc_livecycle_sequence_diagram.png b/assets/eip-6123/doc/sdc_livecycle_sequence_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..253bdfe1f8ad04c416cb65770d199106287ada1d GIT binary patch literal 182042 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

w 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

Z(C*~59G9E9mRe_FVShDI)6abzY{Q{9>Z*s3 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 z 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 { + + // Define objects for TradeState enum, since solidity enums cannot provide their member names... + const TradeState = { + Inactive: 0, + Incepted: 1, + Confirmed: 2, + Active: 3, + Terminated: 4, + }; + + const abiCoder = new AbiCoder(); + const trade_data = "here are the trade specification { + const [_tokenManager, _counterparty1, _counterparty2] = await ethers.getSigners(); + tokenManager = _tokenManager; + counterparty1 = _counterparty1; + counterparty2 = _counterparty2; + const ERC20Factory = await ethers.getContractFactory("SDCToken"); + const SDCFactory = await ethers.getContractFactory("SDC"); + token = await ERC20Factory.deploy(); + await token.deployed(); + sdc = await SDCFactory.deploy(counterparty1.address, counterparty2.address,counterparty1.address, token.address,marginBufferAmount,terminationFee); + await sdc.deployed(); + console.log("SDC Address: %s", sdc.address); + }); + + it("Initial minting and approvals for SDC", async () => { + await token.connect(counterparty1).mint(counterparty1.address,initialLiquidityBalance); + await token.connect(counterparty2).mint(counterparty2.address,initialLiquidityBalance); + await token.connect(counterparty1).approve(sdc.address,terminationFee+marginBufferAmount); + await token.connect(counterparty2).approve(sdc.address,terminationFee+marginBufferAmount); + let allowanceSDCParty1 = await token.connect(counterparty1).allowance(counterparty1.address, sdc.address); + let allowanceSDCParty2 = await token.connect(counterparty2).allowance(counterparty2.address, sdc.address); + await expect(allowanceSDCParty1).equal(terminationFee+marginBufferAmount); + }); + + it("Counterparty1 incepts a trade", async () => { + const incept_call = await sdc.connect(counterparty1).inceptTrade(trade_data,"initialMarketData"); + let tradeid = await sdc.connect(counterparty1).getTradeID(); + //console.log("TradeId: %s", tradeid); + await expect(incept_call).to.emit(sdc, "TradeIncepted").withArgs(counterparty1.address,tradeid,trade_data); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + await expect(trade_state).equal(TradeState.Incepted); + }); + + + it("Counterparty2 confirms a trade", async () => { + const confirm_call = await sdc.connect(counterparty2).confirmTrade(trade_data,"initialMarketData"); + //console.log("TradeId: %s", await sdc.callStatic.getTradeState()); + let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); + await expect(confirm_call).to.emit(sdc, "TradeConfirmed"); + await expect(balanceSDC).equal(2*terminationFee); + let trade_state = await sdc.connect(counterparty1).getTradeState(); + await expect(trade_state).equal(TradeState.Active); + }); + + it("Processing first prefunding phase", async () => { + const call = await sdc.connect(counterparty2).initiatePrefunding(); + let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); + let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); + await expect(balanceSDC).equal(2*(terminationFee+marginBufferAmount)); + await expect(balanceCP2).equal(initialLiquidityBalance-(terminationFee+marginBufferAmount)); + await expect(call).to.emit(sdc, "ProcessFunded"); + }); + + it("Initiate and perform first successful settlement in favour to counterparty 1", async () => { + const callInitSettlement = await sdc.connect(counterparty2).initiateSettlement(); + await expect(callInitSettlement).to.emit(sdc, "ProcessSettlementRequest"); + let balanceCP1 = parseInt(await token.connect(counterparty1).balanceOf(counterparty1.address)); + let balanceCP2 = parseInt(await token.connect(counterparty2).balanceOf(counterparty1.address)); + const callPerformSettlement = await sdc.connect(counterparty2).performSettlement(settlementAmount1,"settlementData"); + await expect(callPerformSettlement).to.emit(sdc, "ProcessSettled"); + let balanceSDC_afterSettlement = await token.connect(counterparty2).balanceOf(sdc.address); + let balanceCP1_afterSettlement = await token.connect(counterparty1).balanceOf(counterparty1.address); + let balanceCP2_afterSettlement = await token.connect(counterparty2).balanceOf(counterparty2.address); + await expect(balanceSDC_afterSettlement).equal(2*(terminationFee+marginBufferAmount)-settlementAmount1); // SDC balance less settlement + await expect(balanceCP1_afterSettlement).equal(balanceCP1+settlementAmount1); // settlement in favour to CP1 + await expect(balanceCP2_afterSettlement).equal(balanceCP2); // CP2 balance is not touched as transfer is booked from SDC balance + }); + + it("Process successfully second prefunding phase successful ", async () => { + await token.connect(counterparty2).approve(sdc.address,settlementAmount1); // CP2 increases allowance + const call = await sdc.connect(counterparty1).initiatePrefunding(); //Prefunding: SDC transfers missing gap amount from CP2 + let balanceSDC = await token.connect(counterparty2).balanceOf(sdc.address); + let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); + await expect(balanceSDC).equal(2*(terminationFee+marginBufferAmount)); + await expect(balanceCP2).equal(initialLiquidityBalance-(terminationFee+marginBufferAmount)-settlementAmount1); + await expect(call).to.emit(sdc, "ProcessFunded"); + }); + + + it("Second settlement fails due to high transfer amount in favour to counteparty 2 - Trade terminates", async () => { + const callInitSettlement = await sdc.connect(counterparty2).initiateSettlement(); + await expect(callInitSettlement).to.emit(sdc, "ProcessSettlementRequest"); + const callPerformSettlement = await sdc.connect(counterparty2).performSettlement(settlementAmount2,"settlementData"); + await expect(callPerformSettlement).to.emit(sdc, "TradeTerminated"); + + let balanceSDC = parseInt(await token.connect(counterparty2).balanceOf(sdc.address)); + let balanceCP1 = await token.connect(counterparty1).balanceOf(counterparty1.address); + let balanceCP2 = await token.connect(counterparty2).balanceOf(counterparty2.address); + let expectedBalanceCP1 = initialLiquidityBalance + settlementAmount1 - (marginBufferAmount + terminationFee); //CP1 received settlementAmount1 and paid margin buffer and termination fee + let expectedBalanceCP2 = initialLiquidityBalance - settlementAmount1 + (marginBufferAmount + terminationFee); //CP2 paid settlementAmount1 and receives margin buffer and termination fee + await expect(balanceCP1).equal(expectedBalanceCP1); + await expect(balanceCP2).equal(expectedBalanceCP2); + await expect(balanceSDC).equal(0); + }); + + + +}); \ No newline at end of file From 7cac73fa988b939af4f5bfb15b0dbaca4ef66a20 Mon Sep 17 00:00:00 2001 From: Simon Brown Date: Mon, 26 Dec 2022 16:24:03 +0000 Subject: [PATCH 083/274] Add EIP-5902: Smart Contract Event Hooks (#5902) * Adding EIP-5897 | Smart Contract Event Hooks Standard * Updated GitHub username to triangular brackets * Fixed markdown linting errors * Updated order of sections to move rationale after specification * Reverted change of EIP numbers to links * Fixed 'markdown-link-first' linting errors * Fixed filename case-sensitity issue in linking to existing EIP * Update EIP number to 5902 * Amended readme file * Updated code examples * Updated format for Hook event payload * Fix some formatting stuff Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5902.md | 641 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 641 insertions(+) create mode 100644 EIPS/eip-5902.md diff --git a/EIPS/eip-5902.md b/EIPS/eip-5902.md new file mode 100644 index 00000000000000..e9ffe7073892d5 --- /dev/null +++ b/EIPS/eip-5902.md @@ -0,0 +1,641 @@ +--- +eip: 5902 +title: Smart Contract Event Hooks +description: Format that allows contracts to semi-autonoumously respond to events emitted by other contracts +author: Simon Brown (@orbmis) +discussions-to: https://ethereum-magicians.org/t/idea-smart-contract-event-hooks-standard/11503 +status: Draft +type: Standards Track +category: ERC +created: 2022-11-09 +requires: 712 +--- + +## Abstract + +This EIP proposes a standard for creating "hooks" that allow a smart contract function to be called automatically in response to a trigger fired by another contract, by using a public relayer network as a messaging bus. + +While there are many similar solutions in existence already, this proposal describes a simple yet powerful primitive that can be employed within many applications in an open, permissionless and decentralized manner. + +It relies on two interfaces, one for a publisher contract and one for a subscriber contract. The publisher contract emits events that are picked up by "relayers", who are independent entities that subscribe to hook events on publisher contracts, and call a function on the respective subscriber contracts whenever a hook event is fired by the publisher contracts. When a relayer calls the respective subscriber's contract with the details of the hook event emitted by the publisher contract, they are paid a fee by the subscriber. Both the publisher and subscriber contracts are registered in a central registry smart contract that relayers can use to discover hooks. + +## Motivation + +There exists a number of use cases that require some off-chain party to monitor the chain and respond to on-chain events by broadcasting a transaction. Such cases usually require some off-chain process to run alongside an Ethereum node, in order to subscribe to events via a web socket connection, and perform some logic in response to an event, by broadcasting a respective transaction to the network. For some use-cases, this may require an Ethereum node and an open websocket connection to some long-running process that may only be used infrequently, resulting in a sub-optimal use of resources. + +This proposal would allow for a smart contract to contain the logic it needs to respond to events without having to store that logic in some off-chain process. The smart contract can subscribe to events fired by other smart contracts and would only execute the required logic when it is needed. This method would suit any contract logic that does not require off-chain computation, but requires an off-chain process to monitor chain state in order to call one of its functions in response. + +Firing hooks from publisher smart contracts still requires some off-chain impetus. To put it another way, somebody has to pull the trigger on the publisher contract, by submitting a transaction to the publisher contract in order to emit the hook event. This is how it works today, and this proposal doesn't change that. Where it does offer an improvement, is that each subscriber no longer needs its own dedicated off-chain process for monitoring and responding to these events. Instead, a single incentivized relayer can subscribe to many different events on behalf of multiple subscriber contracts. + +Thanks to innovations such as web3 webhooks from Moralis, web3 actions from Tenderly, or hal.xyz, creating a relayer is easier than ever. + +Examples of use cases that would benefit from this scheme include: + +### Collateralised lending protocols + +For example, Maker uses the "medianizer" smart contract which maintains a whitelist of price feed contracts which are allowed to post price updates. Every time a new price update is received, the median of all feed prices is re-computed and the medianized value is updated. In this case, the medianizer smart contract could fire a hook event that would allow subscriber contracts to decide to re-collateralize their positions. + +### Automated market makers + +AMM liquidity pools could fire a hook event whenever liquidity is added or removed. This could allow a subscriber smart contracts to add or remove liquidity once the total pool liquidity reaches a certain point. + +AMMs can fire a hook whenever there is a trade within a trading pair, emitting the time-weighted-price-oracle update via an hook event. Subscribers can use this to create an automated Limit-Order-Book contract to buy/sell tokens once an asset's spot price breaches a pre-specified threshold. + +### DAO voting + +Hook events can be emitted by a DAO governance contract to signal that a proposal has been published, voted on, carried or vetoed, and would allow any subscriber contract to automatically respond accordingly. + +### Scheduled function calls + +A scheduler service can be created whereby a subscriber can register for a scheduled funtion call, this could be done using unix cron format and the service can fire events from a smart contract on separate threads. Subscriber contracts can subscriber to the respective threads in order to subscribe to certain schedules (e.g. daily, weekly, hourly etc.), and could even register customer cron schedules. + +### Coordination via Delegation + +Hook event payloads can contain any arbitrary data, this means you can use things like the Delegatable framework to sign off-chain delegations which can faciliate a chain of authorized entities to publish valid Hook events. You can also use things like BLS threshold signatures. + +## 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. + +### Registering a Publisher + +Both the publisher and subscriber contracts **MUST** register in a specific register contract, similarly to how smart contracts register an interface in the [EIP-1820](./eip-1820.md) contract. + +To register a hook in a publisher contract, the `registerHook` function **MUST** be called on the registry contract. The parameters that need to be supplied are: + + - `address` - The publisher contract address, in the form of an ethereum address + - `bytes32` - The public key associated with the hook events + - `uint256` - The thread id that the hooks events will reference (a single contract can fire hook events with any number of threads, subscribers can choose which threads to subscribe to) + +When the `registerHook` function is called on the registry contract, the registry contract **MUST** make a downstream call to the publisher contract address, by calling the publisher contract's `verifyEventHookRegistration` function, with the same arguments as passed to the `registerHook` function on the registry contract. The `verifyEventHookRegistration` function in the publisher contract **MUST** return `true` to indicate that the contract will allow itself to be added to the registry as a publisher. The registry contract **MUST** emit a `HookRegistered` event to indicate that a new publisher contract has been added. + +### Updating a Publisher + +Publishers may want to revoke or update public keys associated with a hook event, or indeed remove support for a hook event completely. The registry contract **MUST** implement the `updatePublisher` function to allow for an existing publisher contract to be updated in the registry. The registry contract **MUST** emit a `PublisherUpdated` event to indicate that the publisher contract was updated. + +### Registering a Subscriber + +To register a subscriber to a hook, the `registerSubscriber` function **MUST** be called on the registry contract with the following parameters: + + - `address` - The publisher contract address + - `bytes32` - The subscriber contract address + - `uint256` - The thread id to subscribe to + - `uint256` - the fee that the subscriber is willing to pay to get updates + - `uint256` - the maximum gas that the subscriber will allow for updates, to prevent griefing attacks + - `uint256` - the maximum gas price that the subscriber is willing to rebate, or 0 to indicate no rebates, in which case it assumed the relay fee covers gas fees + - `uint256` - the chain id that the subscriber wants updates on + - `address` - the address of the token that the fee will be paid in or 0x0 for the chain's native asset (e.g. ETH, MATIC etc.) + +The subscriber contract **MAY** implement gas refunds on top of the fixed fee per update. When a subscriber chooses to do this, they **SHOULD** specify the `maximum gas` and `maximum gas price` parameters in order to protect themselves from griefing attacks. This is so that a malicious or careless relay doesn't set an exorbitantly high gas price and ends up draining the subscriber contracts. Subscriber contracts can otherwise choose to set a fee that is estimated to be sufficiently high to cover gas fees, but they will need to take care to check that the specified gas price does not effectively reduce the fee to zero (see the note under front-running below for a more detailed explanation). + +Note that while the chain ID and the token address were not included in the original version of the spec, the simple addition of these two parameters allows for cross chain messages, should the subscriber wish to do this, and also allows for payment in various tokens. + +### Updating a subscriber + +To update a subscription, the `updateSubscriber` function **MUST** be called with the same set of parameters as the `registerSubscriber` function. This might be done in order to cancel a subscription, or to change the subscription fee. Note that if the average gas fees on a network change over time, the subscription fee might not be enough to incentivise relayers to notify the subscribers of hook events, so in this case the subscription fee might want to be updated periodically. Note that the `updateSubscriber` function **MUST** maintain the same `msg.sender` that the `registerSubscriber` function was called with. + +### Publishing an event + +A publisher contract **SHOULD** emit a hook event from at least one function. The emitted event **MUST** be called `Hook` and **MUST** contain the following parameters: + + - `uint256 indexed` threadId + - `uint256 indexed` nonce + - `bytes32` digest + - `bytes` payload + - `bytes32` checksum + +The `nonce` value **MUST** be incremented every time a `Hook` event is fired by a publisher contract. Every `Hook` event **MUST** have a unique `nonce` value. The `nonce` property is initialized to `1`, but the first `Hook` event **MUST** have a nonce of `2`, to allow for simpler logic in initiating and auto-incremental state variable. + +The `digest` parameter of the event **MUST** be the keccak256 hash of the payload, and the `checksum` **MUST** be the keccak256 hash of the concatenation of the `digest` with the current block number, e.g.: + +```solidity +bytes32 checksum = keccak256(abi.encodePacked(digest, block.number)); +``` + +The function in the publisher contract that emits the `Hook` event **MAY** be passed a signature from an EOA that calls the function. This signature **MUST** be verified by the subscriber's contracts. When using this approach, the signature **SHOULD** be placed at the start of the payload (e.g. bytes `0` to `65` for an ECDSA signature with `r`, `s`, and `v` properties). + +The publisher contract **MAY** emit a `Hook` event without a signature, which allows the `Hook` event to be triggered by a function call from ANY EOA or external contract, and allows the payload to be created dynamically within the publisher contract. In this case the subscriber contract **SHOULD** call the `verifyEventHook` function on the publisher contract to verify that the received Hook payload is valid. + +The payload **MAY** be passed to the function firing the event or **MAY** be generated by the contract itself, but if a signature is provided, it **MUST** sign a hash of the payload, and it is strongly recommended to use the [EIP-712](./eip-712.md) standard as described in the "Replay Attacks" section below. This signature **SHOULD** be verified by the subscribers to ensure they are getting authentic events. The signature **MUST** correspond to the public key that was registered with the event. + +The payload **MUST** be passed as a byte array in calldata. The subscriber smart contract **SHOULD** convert the byte array into the required data type. For example, if the payload is a snark proof, the actual payload might look something like: + +- `uint256[2]` a +- `uint256[2][2]` b +- `uint256[2]` c +- `uint256[1]` input + +In this case the publisher would need to serialize the variables into a bytes32 array, and the subscriber smart contract would need to deserialize it on the other end, e.g.: + +```text +a[0] = uint256(bytes32(payload[0:32])); +a[1] = uint256(bytes32(payload[32:64])); +b[0][0] = uint256(bytes32(payload[64:96])); +b[0][1] = uint256(bytes32(payload[96:128])); +b[1][0] = uint256(bytes32(payload[128:160])); +b[1][1] = uint256(bytes32(payload[160:192])); +c[0] = uint256(bytes32(payload[192:224])); +c[1] = uint256(bytes32(payload[224:256])); +input[0] = uint256(bytes32(payload[256:288])); +``` + +### Relayers + +Relayers are independent parties that listen to `Hook` events on publisher smart contracts. Relayers retrieve a list of subscribers for different hooks from the registry, and listen for hook events being fired on the publisher contracts. Once a hook event has been fired by a publisher smart contract, relayers can decide to relay the hook event's payload to the subscriber contracts by broadcasting a transaction that calls the subscriber contract's `verifyHook` function. Relayers are incentivised to do this because it is expected that the subscriber contract will remunerate them with ETH, or potentially some other asset. + +Relayers **SHOULD** simulate the transaction locally before broadcasting it to make sure that the contract has sufficient balance for payment of the fee. This requires subscriber contracts to maintain a balance of ETH in order to provision payment of relayer fees. A subscriber contract **MAY** decide to revert a transaction based on some logic, which subsequently allows the subscriber contract to conditionally respond to events, depending on the data in the payload. In this case the relayer will simulate the transaction locally and determine not to relay the Hook event to the publisher contract. + +### Verifying a hook event + +The `verifyHook` function of the subscriber contracts **SHOULD** include logic to ensure that they are retrieving authentic events. In the case where the Hook event contains a signature, then subscriber contracts **SHOULD** create a hash of the required parameters, and **SHOULD** verify that the signature in the hook event is valid against the derived hash and the publisher's public key (see the [EIP-712](./eip-712.md) example for reference). The hook function **SHOULD** also verify the nonce of the hook event and record it internally, in order to prevent replay attacks. + +For Hook events without signatures, the subscriber contract **SHOULD** call the `verifyHookEvent` on the publisher contract in order to verify that the hook event is valid. The publisher smart contract **MUST** implement the `verifyHookEvent`, which accepts the hash of the payload, the thread id, the nonce, and the block height associated with the Hook event, and returns a boolean value to indicate the Hook event's authenticity. + +### Interfaces + +#### `IRegistry` + +```solidity +/// @title IRegistry +/// @dev Implements the registry contract +interface IRegistry { + /// @dev Registers a new hook event by a publisher + /// @param publisherContract The address of the publisher contract + /// @param threadId The id of the thread these hook events will be fired on + /// @return Returns true if the hook is successfully registered + function registerHook(address publisherContract, uint256 threadId) external returns (bool); + + /// @dev Verifies a hook with the publisher smart contract before adding it to the registry + /// @param publisherAddress The address of the publisher contract + /// @param threadId The id of the thread these hook events will be fired on + /// @return Returns true if the hook is successfully verified + function verifyHook(address publisherAddress, uint256 threadId) external returns (bool); + + /// @dev Update a previously registered hook event + /// @dev Can be used to transfer hook authorization to a new address + /// @dev To remove a hook, transfer it to the burn address + /// @param publisherContract The address of the publisher contract + /// @param publisherPubKey The public key used to verify the hook signatures + /// @param threadId The id of the thread these hook events will be fired on + /// @return Returns true if the hook is successfully updated + function updateHook( + address publisherContract, + address publisherPubKey, + uint256 threadId + ) external returns (bool); + + /// @dev Registers a subscriber to a hook event + /// @param publisherContract The address of the publisher contract + /// @param subscriberContract The address of the contract subscribing to the event hooks + /// @param threadId The id of the thread these hook events will be fired on + /// @param fee The fee that the subscriber contract will pay the relayer + /// @param maxGas The maximum gas that the subscriber allow to spend, to prevent griefing attacks + /// @param maxGasPrice The maximum gas price that the subscriber is willing to rebate + /// @param chainId The chain id that the subscriber wants updates on + /// @param feeToken The address of the token that the fee will be paid in or 0x0 for the chain's native asset (e.g. ETH) + /// @return Returns true if the subscriber is successfully registered + function registerSubscriber( + address publisherContract, + address subscriberContract, + uint256 threadId, + uint256 fee, + uint256 maxGas, + uint256 maxGasPrice, + uint256 chainId, + address feeToken + ) external returns (bool); + + /// @dev Registers a subscriber to a hook event + /// @param publisherContract The address of the publisher contract + /// @param subscriberContract The address of the contract subscribing to the event hooks + /// @param threadId The id of the thread these hook events will be fired on + /// @param fee The fee that the subscriber contract will pay the relayer + /// @return Returns true if the subscriber is successfully updated + function updateSubscriber( + address publisherContract, + address subscriberContract, + uint256 threadId, + uint256 fee + ) external returns (bool); +} + +``` + +#### `IPublisher` + +```solidity +/// @title IPublisher +/// @dev Implements a publisher contract +interface IPublisher { + /// @dev Example of a function that fires a hook event when it is called + /// @param payload The actual payload of the hook event + /// @param digest Hash of the hook event payload that was signed + /// @param threadId The thread number to fire the hook event on + function fireHook(bytes calldata payload, bytes32 digest, uint256 threadId) external; + + /// @dev Adds / updates a new hook event internally + /// @param threadId The thread id of the hook + /// @param publisherPubKey The public key associated with the private key that signs the hook events + function addHook(uint256 threadId, address publisherPubKey) external; + + /// @dev Called by the registry contract when registering a hook, used to verify the hook is valid before adding + /// @param threadId The thread id of the hook + /// @param publisherPubKey The public key associated with the private key that signs the hook events + /// @return Returns true if the hook is valid and is ok to add to the registry + function verifyEventHookRegistration(uint256 threadId, address publisherPubKey) external view returns (bool); + + /// @dev Returns the address that will sign the hook events on a given thread + /// @param threadId The thread id of the hook + /// @return Returns the address that will sign the hook events on a given thread + function getEventHook(uint256 threadId) external view returns (address); + + /// @dev Returns true if the specified hook is valid + /// @param payloadhash The hash of the hook's data payload + /// @param threadId The thread id of the hook + /// @param nonce The nonce of the current thread + /// @param blockheight The blockheight that the hook was fired at + /// @return Returns true if the specified hook is valid + function verifyEventHook( + bytes32 payloadhash, + uint256 threadId, + uint256 nonce, + uint256 blockheight + ) external view returns (bool); +} + +``` + +#### `ISubscriber` + +```solidity +/// @title ISubscriber +/// @dev Implements a subscriber contract +interface ISubscriber { + /// @dev Example of a function that is called when a hook is fired by a publisher + /// @param publisher The address of the publisher contract in order to verify hook event with + /// @param payload Hash of the hook event payload that was signed + /// @param threadId The id of the thread this hook was fired on + /// @param nonce Unique nonce of this hook + /// @param blockheight The block height at which the hook event was fired + function verifyHook( + address publisher, + bytes calldata payload, + uint256 threadId, + uint256 nonce, + uint256 blockheight + ) external; +} + +``` + +## Rationale + +The rationale for this design is that it allows smart contract developers to write contract logic that listens and responds to events fired in other smart contracts, without requiring them to run some dedicated off-chain process to achieve this. This best suits any simple smart contract logic that runs relatively infrequently in response to events in other contracts. + +This improves on the existing solutions to achieve a pub/sub design pattern. To elaborate: a number of service providers currently offer "webhooks" as a way to subscribe to events emitted by smart contracts, by having some API endpoint called when the events are emitted, or alternatively offer some serverless feature that can be triggered by some smart contract event. This approach works very well, but it does require that some API endpoint or serverless function be always available, which may require some dedicated server / process, which in turn will need to have some private key, and some amount of ETH in order to re-broadcast transactions. + +This approach offers a more suitable alternative for when an "always-on" server instance is not desirable, e.g. in the case that it will be called infrequently. + +This proposal incorporates a decentralized market-driven relay network, and this decision is based on the fact that this is a highly scalable approach. Conversely, it is possible to implement this functionality without resorting to a market-driven approach, by simply defining a standard for contracts to allow other contracts to subscribe directly. That approach is conceptually simpler, but has its drawbacks, in so far as it requires a publisher contract to record subscribers in its own state, creating an overhead for data management, upgradeability etc. That approach would also require the publisher to call the `verifyHook` function on each subscriber contract, which will incur potentially significant gas costs for the publisher contract. + +## Reference Implementation + +### `Registry` + +```solidity +contract Registry is IRegistry { + event HookRegistered( + address indexed publisherContract, + address publisherPubKey, + uint256 threadId, + address result, + bool valid + ); + + event HookUpdated( + address indexed publisherContract, + address publisherPubKey, + uint256 threadId + ); + + event SubscriberRegistered( + address indexed publisherContract, + address indexed subscriberContract, + uint256 threadId, + uint256 fee, + uint256 maxGas, + uint256 maxGasPrice, + uint256 chainId, + address feeToken + ); + + event SubscriberUpdated( + address indexed publisherContract, + address indexed subscriberContract, + uint256 threadId, + uint256 fee + ); + + /// mapping of publisherContractAddress to threadId to publisherPubKey + /// a publisher contract can pubish multiple different hooks on different thread ids + mapping(address => mapping(uint256 => address)) public publishers; + + /// mapping of subscriberContractAddress to publisherContractAddress to threadIds to fee + /// a subscriber contract can subscribe to multiple hook events on one or more contracts + mapping(address => mapping(address => mapping(uint256 => uint256))) public subscribers; + + /// records the owners of a subscriber contract so that updates can be authorized + mapping(address => address) public owners; + + function registerHook(address publisherContract, uint256 threadId) public returns (bool) { + require( + (publishers[publisherContract][threadId] == address(0)), + "Hook already registered" + ); + + address result = IPublisher(publisherContract).getEventHook(threadId); + + bool isHookValid = verifyHook(publisherContract, threadId); + + require(isHookValid, "Hook not valid"); + + // the sender must be the account that signs the hook events + publishers[publisherContract][threadId] = msg.sender; + + emit HookRegistered(publisherContract, msg.sender, threadId, result, isHookValid); + + return true; + } + + function verifyHook(address publisherAddress, uint256 threadId) public view returns (bool) { + return IPublisher(publisherAddress).verifyEventHookRegistration(threadId, msg.sender); + } + + function updateHook( + address publisherContract, + address publisherPubKey, + uint256 threadId + ) public returns (bool) { + require( + publishers[publisherContract][threadId] == msg.sender, + "Not authorized to update hook" + ); + + publishers[publisherContract][threadId] = publisherPubKey; + + emit HookUpdated(publisherContract, publisherPubKey, threadId); + + return true; + } + + function registerSubscriber( + address publisherContract, + address subscriberContract, + uint256 threadId, + uint256 fee, + uint256 maxGas, + uint256 maxGasPrice, + uint256 chainId, + address feeToken + ) public returns (bool) { + require(fee > 0, "Fee must be greater than 0"); + + require( + subscribers[subscriberContract][publisherContract][threadId] != fee, + "Subscriber already registered" + ); + + subscribers[subscriberContract][publisherContract][threadId] = fee; + + owners[subscriberContract] = msg.sender; + + emit SubscriberRegistered(publisherContract, subscriberContract, threadId, fee, maxGas, maxGasPrice, chainId, feeToken); + + return true; + } + + function updateSubscriber( + address publisherContract, + address subscriberContract, + uint256 threadId, + uint256 fee + ) public returns (bool) { + require(owners[subscriberContract] == msg.sender, "Not authorized to update subscriber"); + + subscribers[subscriberContract][publisherContract][threadId] = fee; + + emit SubscriberUpdated(publisherContract, subscriberContract, threadId, fee); + + return true; + } +} +``` + +### `Publisher` + +```solidity +contract Publisher is IPublisher, Ownable { + uint256 public hookNonce = 1; + + // mapping of threadId to nonce to digest (payload data hash) + mapping(uint256 => mapping(uint256 => bytes32)) public firedHooks; + + event Hook( + uint256 indexed threadId, + uint256 indexed nonce, + bytes32 digest, + bytes payload, + bytes32 checksum + ); + + mapping(uint256 => address) public hooks; + + function fireHook( + bytes calldata payload, + bytes32 digest, + uint256 threadId + ) public onlyOwner { + hookNonce++; + + bytes32 checksum = keccak256(abi.encodePacked(digest, block.number)); + + firedHooks[threadId][hookNonce] = checksum; + + emit Hook(threadId, hookNonce, digest, payload, checksum); + } + + function addHook(uint256 threadId, address publisherPubKey) public onlyOwner { + hooks[threadId] = publisherPubKey; + } + + function verifyEventHookRegistration( + uint256 threadId, + address publisherPubKey + ) public view override returns (bool) { + return (hooks[threadId] == publisherPubKey); + } + + function verifyEventHook( + bytes32 payloadhash, + uint256 threadId, + uint256 nonce, + uint256 blockheight + ) external view returns (bool) { + bytes32 checksum = keccak256(abi.encodePacked(payloadhash, blockheight)); + + bool result = firedHooks[threadId][nonce] == checksum; + + return result; + } + + function getEventHook(uint256 threadId) public view returns (address) { + return hooks[threadId]; + } +} +``` + +### `Subscriber` + +```solidity +contract Subscriber is ISubscriber, Ownable { + uint256 public constant RELAYER_FEE = 0.001 ether; + uint256 public constant MAX_AGE = 4; + uint256 public constant STARTING_GAS = 21000; + uint256 public constant VERIFY_HOOK_ENTRY_GAS = 8000; + uint256 public constant VERIFY_HOOK_GAS_COST = 60000; + uint256 public constant MAX_GAS_PRICE = 10000000000; + + uint256 public constant MAX_GAS_ALLOWED = + STARTING_GAS + VERIFY_HOOK_ENTRY_GAS + VERIFY_HOOK_GAS_COST; + + // mapping of publisher address to threadId to nonce + mapping(address => mapping(uint256 => uint256)) public validPublishers; + + receive() external payable {} + + function updateValidPublishers( + address publisher, + uint256 threadId, + uint256 nonce + ) public onlyOwner { + require(nonce > 0, "nonce must be greater than zero"); + validPublishers[publisher][threadId] = nonce; + } + + function getPublisherNonce(address publisher, uint256 threadId) public view returns (uint256) { + return validPublishers[publisher][threadId]; + } + + function verifyHook( + address publisher, + bytes calldata payload, + uint256 threadId, + uint256 nonce, + uint256 blockheight + ) public { + uint256 gasStart = gasleft(); + + bool isHookValid = IPublisher(publisher).verifyEventHook( + keccak256(payload), + threadId, + nonce, + blockheight + ); + + // checks + require(isHookValid, "Hook not verified by publisher"); + require(nonce > validPublishers[publisher][threadId], "Obsolete hook detected"); + require(tx.gasprice <= MAX_GAS_PRICE, "Gas price is too high"); + require(blockheight < block.number, "Hook event not valid yet"); + require((block.number - blockheight) < MAX_AGE, "Hook has expired"); + require(validPublishers[publisher][threadId] != 0, "Publisher not valid"); + + // effects + validPublishers[publisher][threadId] = nonce; + + // interactions + (bool result, ) = msg.sender.call{value: RELAYER_FEE}(""); + + require(result, "Failed to send relayer fee"); + + require( + (gasStart - gasleft()) < MAX_GAS_ALLOWED, + "Function call exceeded gas allowance" + ); + } +} +``` + +## Security Considerations + +### Griefing attacks + +It is imperative that subscriber contracts trust the publisher contracts not to fire events that hold no intrinsic interest or value for them, as it is possible that malicious publisher contracts can publish a large number of events that will in turn drain the ETH from the subscriber contracts. If the private key used to sign the hook events is ever compromised, then the potential to drain ETH from all subscriber contracts is a very real possibility. + +### Front-running attacks + +When using signatures to validate Hook events, it is important for publishers and subscribers of hooks to realize that it is possible for a relayer to relay hook events before they are broadcast, by examining the publisher's originating transaction in the mempool. The normal flow is for the originating transaction to call a function in the publisher smart contract, which in turn fires an event which is then picked up by relayers. Competitive relayers will observe that it is possible to pluck the signature from the originating transaction from the mempool and simply relay it to subscriber contracts before the originating transaction has been actually included in a block. In fact, it is possible that the subscriber contracts process the event before the originating transaction is processed, based purely on gas fee dynamics. This can mitigated against by subscriber contracts calling the `verifyEventHook` function on the publisher contract when they receive a Hook event. + +Another risk from front-running affects relayers, whereby the relayer's transactions to the subscriber contracts can be front-run by generalized MEV searchers in the mempool. It is likely that this sort of MEV capture will occur in the public mempool, and therefore it is advised that relayers use private channels to block builders to mitigate against this issue. By broadcasting transactions to a segregated mempool, relayers protect themselves from front-running by generalized MEV bots, but their transactions can still fail due to competition from other relayers. If two or more relayers decide to start relaying hook events from the same publisher, then the relay transactions with the highest gas price will be executed before the others. This will result in the other relayer's transactions potentially failing on-chain, by being included later in the same block. For now, there are certain transaction optimization services that will prevent transactions from failing on-chain, which will offer a solution to this problem, though this is out-of-scope for this document. A future iteration of this proposal may well include the option for trusted relayers, who can enter into an on-chain enforceable agreement with subscribers, which should reduce the race-to-the-bottom competitive gas fee issue. + +In order to cultivate and maintain a reliable relayer market, it is recommended that where possible, a subscriber contract implements logic to either rebate any gas fees up to a specified limit, (while still allowing for execution of hook updates under normal conditions), or implements a logical condition that checks that the gas price of the transaction that is calling the `verifyHook` function to ensure that the gas price does not effectively reduce the fee to zero. This would require that the smart contract have some knowledge of the approximate gas used by the `verifyHook` function, and checks that the condition `minFee >= fee - (gasPrice * gasUsed)`. This will mitigate against competitive bidding that would drive the _effective_ relayer fee to zero, by ensuring that there is some minimum fee below which the effective fee is not allowed to drop. This would mean that the highest gas price that can be paid before the transaction reverts is `fee - minFee + ε` where `ε ~= 1 gwei`. This will require careful estimation of the gas cost of the `verifyHook` function and an awareness that the gas used may change over time as the contract's state changes. + +Another important consideration is with batching of Hook events. If a relayer decides to batch multiple Hook event updates to various subscriber contracts into a single transaction, via a multi-call proxy contract, then they increase the risk of the entire batching failing on-chain. For example, if relayer A batches x number of Hook updates, and relayer B batches y number of Hook updates, it is possible that relayer A's batch is included in the same block in front of relayer B's batch, and if both batches contain at least one duplicate, (i.e. the same Hook event to the same subscriber), then this will cause relayer B entire batch transaction to revert on-chain. This is an inportant consideration for relayers. + +### Replay attacks + +When using signature verification, it is advised to use the [EIP-712](./eip-712.md) standard in order to prevent cross network replay attacks, where the same contract deployed on more than one network can have its hook events pushed to subscribers on other networks, e.g. a publisher contract on Polygon can fire an hook event that could be relayed to a subscriber contract on Gnosis Chain. Whereas the keys used to sign the hook events should ideally be unique, in reality this may not always be the case. + +For this reason, it is recommended to use [EIP-721](./eip-712.md) Typed Data Signatures. In this case the off-chain process that initiates the hook should create the signature according to the following data structure: + +```solidity +const domain = [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + { name: "salt", type: "bytes32" } +] + +const hook = [ + { name: "payload", type: "string" }, + { type: "uint256", name: "nonce" }, + { type: "uint256", name: "blockheight" }, + { type: "uint256", name: "threadId" }, +] + +const domainData = { + name: "Name of Publisher Dapp", + version: "1", + chainId: parseInt(web3.version.network, 10), + verifyingContract: "0x123456789abcedf....publisher contract address", + salt: "0x123456789abcedf....random hash unique to publisher contract" +} + +const message = { + payload: "bytes array serialized payload" + nonce: 1, + blockheight: 999999, + threadId: 1, +} + +const eip712TypedData = { + types: { + EIP712Domain: domain, + Hook: hook + }, + domain: domainData, + primaryType: "Hook", + message: message +} +``` + +Note: please refer to the unit tests for an example of how a hook event should be constructed properly by the publisher. + +Replay attacks can also occur on the same network that the event hook was fired, by simply re-broadcasting an event hook that was already broadcast previously. For this reason, subscriber contracts should check that a nonce is included in the event hook being received, and record the nonce in the contract's state. If the hook nonce is not valid, or has already been recorded, the transaction should revert. + +It is worth noting that the `chainId` event topic should also be used to prevent cross chain replay attacks, in the case that a dapp is deployed on multiple networks. There is also the possibility to leverage the `chainId` for more than preventing replay attacks, but also for accepting messages from other chains. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 6133a6c1d180aa9d23d7b3614aaea42f8777ef63 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Mon, 26 Dec 2022 11:31:05 -0500 Subject: [PATCH 084/274] Update EIP-5507: Add interface IDs and use consistent formatting (#6218) --- EIPS/eip-5507.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index e91fc9180836c7..6d6c987de77290 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -28,14 +28,14 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ```solidity // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.4; +pragma solidity ^0.8.17; import "IERC20.sol"; import "IERC165.sol"; /// @notice Refundable EIP-20 tokens -/// @dev The EIP-165 identifier of this interface is `0xTODO` -interface IERC20Refund is ERC20, ERC165 { +/// @dev The EIP-165 identifier of this interface is `0xf0ca2917` +interface ERC20Refund is ERC20, ERC165 { /// @notice Emitted when a token is refunded /// @dev Emitted by `refund` /// @param _sender The person that requested a refund @@ -65,14 +65,14 @@ interface IERC20Refund is ERC20, ERC165 { ```solidity // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.4; +pragma solidity ^0.8.17; import "IERC721.sol"; import "IERC165.sol"; /// @notice Refundable EIP-721 tokens -/// @dev The EIP-165 identifier of this interface is `0xTODO` -interface IERC721Refund is ERC721, ERC165 { +/// @dev The EIP-165 identifier of this interface is `0xe97f3c83` +interface ERC721Refund is ERC721 /* , ERC165 */ { /// @notice Emitted when a token is refunded /// @dev Emitted by `refund` /// @param _sender The person that requested a refund @@ -104,14 +104,14 @@ interface IERC721Refund is ERC721, ERC165 { ```solidity // SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.4; +pragma solidity ^0.8.17; import "IERC1155.sol"; import "IERC165.sol"; /// @notice Refundable EIP-1155 tokens -/// @dev The EIP-165 identifier of this interface is `0xTODO` -interface IERC1155Refund is IERC1155, IERC165 { +/// @dev The EIP-165 identifier of this interface is `0x94029f5c` +interface ERC1155Refund is ERC1155 /* , ERC165 */ { /// @notice Emitted when a token is refunded /// @dev Emitted by `refund` /// @param _sender The person that requested a refund From 16f7cb55c21057b9edd2f6f1f6337f46bf172974 Mon Sep 17 00:00:00 2001 From: Suning Yao Date: Mon, 26 Dec 2022 18:41:44 -0500 Subject: [PATCH 085/274] Add EIP-6150: Hierarchical NFTs (#6150) * Add EIP: Hierarchical NFTs * Update EIP number * Update discussion link * Fix markdown linter check failure * Fix markdown linter check failure * Fix EIP-6150 implementation link * Update demo implementation --- EIPS/eip-6150.md | 286 ++++++++++++++++++++++++ assets/eip-6150/contracts/ERC-6150.sol | 122 ++++++++++ assets/eip-6150/contracts/IERC-6150.sol | 47 ++++ assets/eip-6150/linux-hierarchy.png | Bin 0 -> 12588 bytes assets/eip-6150/website-hierarchy.png | Bin 0 -> 162576 bytes 5 files changed, 455 insertions(+) create mode 100644 EIPS/eip-6150.md create mode 100644 assets/eip-6150/contracts/ERC-6150.sol create mode 100644 assets/eip-6150/contracts/IERC-6150.sol create mode 100644 assets/eip-6150/linux-hierarchy.png create mode 100644 assets/eip-6150/website-hierarchy.png diff --git a/EIPS/eip-6150.md b/EIPS/eip-6150.md new file mode 100644 index 00000000000000..3c065319ed5bcf --- /dev/null +++ b/EIPS/eip-6150.md @@ -0,0 +1,286 @@ +--- +eip: 6150 +title: Hierarchical NFTs +description: Hierarchical NFTs, an extension to EIP-721. +author: Keegan Lee (@keeganlee), msfew , Kartin , qizhou (@qizhou) +discussions-to: https://ethereum-magicians.org/t/eip-6150-hierarchical-nfts-an-extension-to-erc-721/12173 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-15 +requires: 165, 721 +--- + +## Abstract + +This standard is an extension to [EIP-721](./eip-721.md). It proposes a multi-layer filesystem-like hierarchical NFTs. This standard provides interfaces to get parent NFT or children NFTs and whether NFT is a leaf node or root node, maintaining the hierarchical relationship among them. + +## Motivation + +This EIP standardizes the interface of filesystem-like hierarchical NFTs and provides a reference implementation. + +Hierarchy structure is commonly implemented for file systems by operating systems such as Linux Filesystem Hierarchy (FHS). + +![Linux Hierarchical File Structure](../assets/eip-6150/linux-hierarchy.png) + +Websites often use a directory and category hierarchy structure, such as eBay (Home -> Electronics -> Video Games -> Xbox -> Products), and Twitter (Home -> Lists -> List -> Tweets), and Reddit (Home -> r/ ethereum -> Posts -> Hot). + +![Website Hierarchical Structure](../assets/eip-6150/website-hierarchy.png) + +A single smart contract can be the `root`, managing every directory/category as individual NFT and hierarchy relations of NFTs. Each NFT's `tokenURI` may be another contract address, a website link, or any form of metadata. + +The advantages and the advancement of the Ethereum ecosystem of using this standard include: + +- Complete on-chain storage of hierarchy, which can also be governed on-chain by additional DAO contract +- Only need a single contract to manage and operate the hierarchical relations +- Transferrable directory/category ownership as NFT, which is great for use cases such as on-chain forums +- Easy and permissionless data access to the hierarchical structure by front-end +- Ideal structure for traditional applications such as e-commerce, or forums +- Easy-to-understand interfaces for developers, which are similar to Linux filesystem commands in concept + +The use cases can include: + +- On-chain forum, like Reddit +- On-chain social media, like Twitter +- On-chain corporation, for managing organizational structures +- On-chain e-commerce platforms, like eBay or individual stores +- Any application with tree-like structures + +In the future, with the development of the data availability solutions of Ethereum and an external permissionless data retention network, the content (posts, listed items, or tweets) of these platforms can also be entirely stored on-chain, thus realizing fully decentralized applications. + +## Specification + +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. + +Every [EIP-6150](./eip-6150.md) compliant contract must implement the [EIP-6150](./eip-6150.md), [EIP-721](./eip-721.md) and [EIP-165](./eip-165.md) interfaces. + +```solidity +pragma solidity ^0.8.0; + +interface IERC6150 /* is IERC721, IERC165 */ { + /** + * @notice Emitted when `tokenId` token under `parentId` is minted. + * @param minter The address of minter + * @param to The address received token + * @param parentId The id of parent token, if it's zero, it means minted `tokenId` is a root token. + * @param tokenId The id of minted token, required to be greater than zero + */ + event Minted( + address indexed minter, + address indexed to, + uint256 parentId, + uint256 tokenId + ); + + /** + * @notice Get the parent token of `tokenId` token. + * @param tokenId The child token + * @return parentId The Parent token found + */ + function parentOf(uint256 tokenId) external view returns (uint256 parentId); + + /** + * @notice Get the children tokens of `tokenId` token. + * @param tokenId The parent token + * @return childrenIds The array of children tokens + */ + function childrenOf( + uint256 tokenId + ) external view returns (uint256[] memory childrenIds); + + /** + * @notice Check the `tokenId` token if it is a root token. + * @param tokenId The token want to be checked + * @return Return `true` if it is a root token; if not, return `false` + */ + function isRoot(uint256 tokenId) external view returns (bool); + + /** + * @notice Check the `tokenId` token if it is a leaf token. + * @param tokenId The token want to be checked + * @return Return `true` if it is a leaf token; if not, return `false` + */ + function isLeaf(uint256 tokenId) external view returns (bool); +} +``` + +Optional Extension: Enumerable + +```solidity +interface IERC6150Enumerable is IERC6150 /* IERC721Enumerable */ { + /** + * @notice Get total amount of children tokens under `parentId` token. + * @dev If `parentId` is zero, it means get total amount of root tokens. + * @return The total amount of children tokens under `parentId` token. + */ + function childrenCountOf(uint256 parentId) external view returns (uint256); + + /** + * @notice Get the token at the specified index of all children tokens under `parentId` token. + * @dev If `parentId` is zero, it means get root token. + * @return The token ID at `index` of all chlidren tokens under `parentId` token. + */ + function childOfParentByIndex( + uint256 parentId, + uint256 index + ) external view returns (uint256); + + /** + * @notice Get the index position of specified token in the children enumeration under specified parent token. + * @dev Throws if the `tokenId` is not found in the children enumeration. + * If `parentId` is zero, means get root token index. + * @param parentId The parent token + * @param tokenId The specified token to be found + * @return The index position of `tokenId` found in the children enumeration + */ + function indexInChildrenEnumeration( + uint256 parentId, + uint256 tokenId + ) external view returns (uint256); +} +``` + +Optional Extension: Burnable + +```solidity +interface IERC6150Burnable is IERC6150 { + /** + * @notice Burn the `tokenId` token. + * @dev Throws if `tokenId` is not a leaf token. + * Throws if `tokenId` is not a valid NFT. + * Throws if `owner` is not the owner of `tokenId` token. + * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this token. + * @param tokenId The token to be burnt + */ + function safeBurn(uint256 tokenId) external; + + /** + * @notice Batch burn tokens. + * @dev Throws if one of `tokenIds` is not a leaf token. + * Throws if one of `tokenIds` is not a valid NFT. + * Throws if `owner` is not the owner of all `tokenIds` tokens. + * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for all `tokenIds`. + * @param tokenIds The tokens to be burnt + */ + function safeBatchBurn(uint256[] memory tokenIds) external; +} +``` + +Optional Extension: ParentTransferable + +```solidity +interface IERC6150ParentTransferable is IERC6150 { + /** + * @notice Emitted when the parent of `tokenId` token changed. + * @param tokenId The token changed + * @param oldParentId Previous parent token + * @param newParentId New parent token + */ + event ParentTransferred( + uint256 tokenId, + uint256 oldParentId, + uint256 newParentId + ); + + /** + * @notice Transfer parentship of `tokenId` token to a new parent token + * @param newParentId New parent token id + * @param tokenId The token to be changed + */ + function transferParent(uint256 newParentId, uint256 tokenId) external; + + /** + * @notice Batch transfer parentship of `tokenIds` to a new parent token + * @param newParentId New parent token id + * @param tokenIds Array of token ids to be changed + */ + function batchTransferParent( + uint256 newParentId, + uint256[] memory tokenIds + ) external; +} +``` + +Optional Extension: Access Control + +```solidity +interface IERC6150AccessControl is IERC6150 { + /** + * @notice Check the account whether a admin of `tokenId` token. + * @dev Each token can be set more than one admin. Admin have permission to do something to the token, like mint child token, + * or burn token, or transfer parentship. + * @param tokenId The specified token + * @param account The account to be checked + * @return If the account has admin permission, return true; otherwise, return false. + */ + function isAdminOf(uint256 tokenId, address account) + external + view + returns (bool); + + /** + * @notice Check whether the specified parent token and account can mint children tokens + * @dev If the `parentId` is zero, check whether account can mint root nodes + * @param parentId The specified parent token to be checked + * @param account The specified account to be checked + * @return If the token and account has mint permission, return true; otherwise, return false. + */ + function canMintChildren( + uint256 parentId, + address account + ) external view returns (bool); + + /** + * @notice Check whether the specified token can be burnt by specified account + * @param tokenId The specified token to be checked + * @param account The specified account to be checked + * @return If the tokenId can be burnt by account, return true; otherwise, return false. + */ + function canBurnTokenByAccount(uint256 tokenId, address account) + external + view + returns (bool); +} +``` + +## Rationale + +As mentioned in the abstract, this EIP's goal is to have a simple interface for supporting Hierarchical NFTs. Here are a few design decisions and why they were made: + +### Relationship between NFTs + +All NFTs will make up a hierarchical relationship tree. Each NFT is a node of the tree, maybe as a root node or a leaf node, as a parent node or a child node. + +[EIP-6150](./eip-6150.md) standardizes the event `Minted` to indicate the parent and child relationship when minting a new node. When a root node is minted, parentId should be zero. That means a token id of zero could not be a real node. So a real node token id must be greater than zero. + +In a hierarchical tree, it's common to query upper and lower nodes. So [EIP-6150](./eip-6150.md) standardizes function `parentOf` to get the parent node of the specified node and standardizes function `childrenOf` to get all children nodes. + +Functions `isRoot` and `isLeaf` can check if one node is a root node or a leaf node, which would be very useful for many cases. + +### Enumerable Extension + +[EIP-6150](./eip-6150.md) standardizes three functions as an extension to support enumerable queries involving children nodes. Each function all have param `parentId`, for compatibility, when the `parentId` specified zero means query root nodes. + +### ParentTransferable Extension + +In some cases, such as filesystem, a directory or a file could be moved from one directory to another. So [EIP-6150](./eip-6150.md) adds ParentTransferable Extension to support this situation. + +### Access Control + +In a hierarchical structure, usually, there is more than one account has permission to operate a node, like mint children nodes, transfer node, burn node. [EIP-6150](./eip-6150.md) adds a few functions as standard to check access control permissions. + +## Backwards Compatibility + +This proposal is fully backward compatible with [EIP-721](./eip-721.md). + +## Reference Implementation + +Implementation: [EIP-6150.sol](../assets/eip-6150/contracts/ERC-6150.sol) + +## Security Considerations + +No security considerations were found. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-6150/contracts/ERC-6150.sol b/assets/eip-6150/contracts/ERC-6150.sol new file mode 100644 index 00000000000000..11fdeb5a31c3b4 --- /dev/null +++ b/assets/eip-6150/contracts/ERC-6150.sol @@ -0,0 +1,122 @@ +pragma solidity ^0.8.0; + +import "./IERC-6150.sol"; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +abstract contract ERC6150 is ERC721, IERC6150 { + mapping(uint256 => uint256) private _parentOf; + mapping(uint256 => uint256[]) private _childrenOf; + + constructor( + string memory name_, + string memory symbol_ + ) ERC721(name_, symbol_) {} + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC721) returns (bool) { + return + interfaceId == type(IERC6150).interfaceId || + super.supportsInterface(interfaceId); + } + + function parentOf( + uint256 tokenId + ) public view virtual override returns (uint256 parentId) { + _requireMinted(tokenId); + parentId = _parentOf[tokenId]; + } + + function childrenOf( + uint256 tokenId + ) public view virtual override returns (uint256[] memory childrenIds) { + _requireMinted(tokenId); + childrenIds = _childrenOf[tokenId]; + } + + function isRoot( + uint256 tokenId + ) public view virtual override returns (bool) { + _requireMinted(tokenId); + return _parentOf[tokenId] == 0; + } + + function isLeaf( + uint256 tokenId + ) public view virtual override returns (bool) { + _requireMinted(tokenId); + return _childrenOf[tokenId].length == 0; + } + + function _safeBatchMintWithParent( + address to, + uint256 parentId, + uint256[] memory tokenIds + ) internal virtual { + _safeBatchMintWithParent( + to, + parentId, + tokenIds, + new bytes[](tokenIds.length) + ); + } + + function _safeBatchMintWithParent( + address to, + uint256 parentId, + uint256[] memory tokenIds, + bytes[] memory datas + ) internal virtual { + require( + tokenIds.length == datas.length, + "EIP6150: tokenIds.length != datas.length" + ); + for (uint256 i = 0; i < tokenIds.length; i++) { + _safeMintWithParent(to, parentId, tokenIds[i], datas[i]); + } + } + + function _safeMintWithParent( + address to, + uint256 parentId, + uint256 tokenId + ) internal virtual { + _safeMintWithParent(to, parentId, tokenId, ""); + } + + function _safeMintWithParent( + address to, + uint256 parentId, + uint256 tokenId, + bytes memory data + ) internal virtual { + require(tokenId > 0, "EIP6150: tokenId is zero"); + if (parentId != 0) + require(_exists(parentId), "EIP6150: parentId doesn't exists"); + + _beforeMintWithParent(to, parentId, tokenId); + + _parentOf[tokenId] = parentId; + _childrenOf[parentId].push(tokenId); + + _safeMint(to, tokenId, data); + emit Minted(msg.sender, to, parentId, tokenId); + + _afterMintWithParent(to, parentId, tokenId); + } + + function _beforeMintWithParent( + address to, + uint256 parentId, + uint256 tokenId + ) internal virtual {} + + function _afterMintWithParent( + address to, + uint256 parentId, + uint256 tokenId + ) internal virtual {} +} diff --git a/assets/eip-6150/contracts/IERC-6150.sol b/assets/eip-6150/contracts/IERC-6150.sol new file mode 100644 index 00000000000000..d4830852a9b425 --- /dev/null +++ b/assets/eip-6150/contracts/IERC-6150.sol @@ -0,0 +1,47 @@ +pragma solidity ^0.8.0; + +interface IERC6150 /* is IERC721, IERC165 */ { + /** + * @notice Emitted when `tokenId` token under `parentId` is minted. + * @param minter The address of minter + * @param to The address received token + * @param parentId The id of parent token, if it's zero, it means minted `tokenId` is a root token. + * @param tokenId The id of minted token, required to be greater than zero + */ + event Minted( + address indexed minter, + address indexed to, + uint256 parentId, + uint256 tokenId + ); + + /** + * @notice Get the parent token of `tokenId` token. + * @param tokenId The child token + * @return parentId The Parent token found + */ + function parentOf(uint256 tokenId) external view returns (uint256 parentId); + + /** + * @notice Get the children tokens of `tokenId` token. + * @param tokenId The parent token + * @return childrenIds The array of children tokens + */ + function childrenOf( + uint256 tokenId + ) external view returns (uint256[] memory childrenIds); + + /** + * @notice Check the `tokenId` token if it is a root token. + * @param tokenId The token want to be checked + * @return Return `true` if it is a root token; if not, return `false` + */ + function isRoot(uint256 tokenId) external view returns (bool); + + /** + * @notice Check the `tokenId` token if it is a leaf token. + * @param tokenId The token want to be checked + * @return Return `true` if it is a leaf token; if not, return `false` + */ + function isLeaf(uint256 tokenId) external view returns (bool); +} diff --git a/assets/eip-6150/linux-hierarchy.png b/assets/eip-6150/linux-hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..60ecc1fc0ed2ad7b4280adc717d166bbc89b7413 GIT binary patch literal 12588 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 literal 0 HcmV?d00001 diff --git a/assets/eip-6150/website-hierarchy.png b/assets/eip-6150/website-hierarchy.png new file mode 100644 index 0000000000000000000000000000000000000000..85215eccf6be5bbf11befbf673ab22882f7702f8 GIT binary patch literal 162576 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 H@ArQIig%E@ literal 0 HcmV?d00001 From e666b5b02c7d05116ca8718aa1f9d12f22c8f5d6 Mon Sep 17 00:00:00 2001 From: Weiji Guo <118721011+weiji-cryptonatty@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:58:25 +0800 Subject: [PATCH 086/274] Add EIP-6051: Private Key Encapsulation (#6051) * added EIP draft for private key encapsulation * minor updates to spec: intake function shall return the Ethereum address of the private key * added test vector #1 * minor formatting * minor edits * added test vector #2 and #3, added signature verification data to #1 * changed signature to against byte values * added test vector generator * renamed file to assigned EIP number * fixed file header * updated default value for salt * fixed offending links etc. * fixed typo Co-authored-by: xinbenlv * updated based on review comments * replaced json formatting with none for better rendering * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * revision suggestions taken with gratitudes Co-authored-by: xinbenlv * revision suggestions taken with gratitudes Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * fixed grammar Co-authored-by: xinbenlv * revision suggestions taken with gratitudes Co-authored-by: xinbenlv * fixed grammar as suggested Co-authored-by: xinbenlv * revision suggestions taken with gratitudes Co-authored-by: xinbenlv * fixed grammar as suggested Co-authored-by: xinbenlv * fixed grammar as suggested Co-authored-by: xinbenlv * fixed grammar as suggested * fixed based on grammarly.com suggestions * Update EIPS/eip-6051.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6051.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-6051.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * replacing bold fonts with links as suggested * fixed dead links * fixed markdown linter errors Co-authored-by: xinbenlv Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6051.md | 523 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 523 insertions(+) create mode 100644 EIPS/eip-6051.md diff --git a/EIPS/eip-6051.md b/EIPS/eip-6051.md new file mode 100644 index 00000000000000..c1f380db909a2e --- /dev/null +++ b/EIPS/eip-6051.md @@ -0,0 +1,523 @@ +--- +eip: 6051 +title: Private Key Encapsulation +description: defines a specification for encapsulating private keys. +author: Base Labs (@Base-Labs), Weiji Guo (@weiji-cryptonatty) +discussions-to: https://ethereum-magicians.org/t/private-key-encapsulation-to-move-around-securely-without-entering-seed/11604 +status: Draft +type: Standards Track +category: Interface +created: 2022-11-21 +--- + + +## Abstract + +This EIP proposes a mechanism to encapsulate a private key so that it could be securely relocated to another application without providing the seed. This EIP combines `ECIES` (Elliptic Curve Integrated Encryption Scheme) and optional signature verification under various choices to ensure that the private key is encapsulated for a known or trusted party. + +## Motivation + +There are various cases in which we might want to export one of many private keys from a much more secure but less convenient wallet, which is controlled with a seed or passphrase. + +1. We might dedicate one of many private keys for messaging purposes, and that private key is probably managed in a not-so-secure manner; +2. We might want to export one of many private keys from a hardware wallet, and split it with MPC technology so that a 3rd party service could help us identify potential frauds or known bad addresses, enforce 2FA, etc., meanwhile we can initiate transactions from a mobile device with much better UX and without carrying a hardware wallet. + +In both cases, it is safer not to provide the seed which controls the whole wallet and might contains many addresses in multiple chains. + +This EIP aims to enable such use cases. + +## Specification + +### Sender and Recipient + +We hereby define: + +- *Sender* as the party who holds in custody the private key to be encapsulated; *Sender Application* as the client-side application that said *Sender* uses to send the encapsulated private key. + +- *Recipient* as the party who accepts the encapsulated private key, unwraps, and then uses it; *Recipient Application* as the client-side application that *Recipient* uses to receive the encapsulated private key. + +### Core Algorithms + +The basic idea is to encapsulate the private key with ECIES. To ensure that the ephemeral public key to encapsulate the private key is indeed generated from a trusted party and has not been tampered with, we also provided an option to sign that ephemeral public key in this standard. + +There should be a mandatory `version` parameter. This allows various kinds of Key Encapsulation Mechanisms to be adopted depending on security considerations or preferences. The list shall be short to minimize compatibility issues among different vendors. + +In addition to a `version` parameter, the following keys and functions are involved: + +1. The Sender's private key `sk`, which is to be encapsulated to the Recipient, and the corresponding address `account`. +2. The ephemeral Recipient key pair `(r, R)` such that `R = [r]G`. `G` denotes the base point of the elliptic curve, and `[r]G` denotes scalar multiplication. Optionally, `R` could be signed, and `signerPubKey` and `signature` are then provided for Sender to verify if `R` could be trusted or not. +3. The ephemeral Sender key pair `(s, S)` such that `S = [s]G`. +4. The share secret `ss := [s]R = [r]S` according to ECDH. Note that for secp256k1 this EIP follows RFC5903 and uses compact representation, which means to use *only* the `x` coordinate as the shared secret. For Curve25519 this EIP follows RFC7748. +5. The out-of-band data `oob`, optional. This could be digits or an alpha-numeric string entered by the user. +6. Let `derivedKey := HKDF(hash=SHA256, ikm=ss, info=oob, salt, length)`. HKDF is defined in RFC5869. The `length` should be determined by `skey` and `IV` requirements such that the symmetric key `skey = derivedKey[0:keySize]`, and `IV = derivedKey[keySize:length]`. `keySize` denotes the key size of the underlying symmetric algorithm, for example, 16 (bytes) for AES-128, and 32 (bytes) for Chacha20. See **Security Considerations** for the use of `salt`. +7. Let `cipher := authenticated_encryption(symAlg, skey, IV, data=sk)`. The symmetric cipher algorithm `symAlg` and authentication scheme are decided by the version parameter. No additional authentication data `aad` is used. + +A much-simplified example flow without signature and verification is: + +1. *Recipient Application* generates `(r, R)`. +2. User inputs `R` to *Sender Application*, along with a six-digit code “123456” as `oob`. +3. *Sender Application* generates `(s, S)`, and computes `cipher`, then returns `S || cipher`. +4. *Recipient Application* scans to read `S` and `cipher`. The user enters “123456” as `oob` to *Recipient Application*. +5. *Recipient Application* decrypts `cipher` to get `sk`. +6. *Recipient Application* derives the address corresponding to `sk` so that the user can confirm the correctness. + +With signature and verification, the signature to `R` by `singerPubKey` is appended to `R`. `signerPubKey` itself could have been already signed by `trustedPubKey`, and that signature is appended to `signerPubKey`. Note that the signature is applied to the byte array data instead of its string representation, which might lead to confusion and interoperability issues (such as hex or base64, lower case v.s. upper case, etc.). See [Requests](#requests) and [Test Cases](#test-cases) for further clarification and examples. + +### Requests + +#### Encoding of data and messages + +- Raw bytes are encoded in hex and prefixed with '0x'. +- Unless specified otherwise, all parameters and return values are hex-encoded bytes. +- `cipher` is encoded into a single byte buffer as: `[IV || encrypted_sk || tag]`. +- `R`, `S`, `signerPubKey`, and `trustedPubKey` are compressed if applicable. +- `R` or `signerPubKey` could be followed by a signature to it: `[pub || sig]`. Note that for the secp256k1 curve, the signature is just 64 bytes without the `v` indicator as found in a typical Ethereum signature. + +#### R1. Request for Recipient to generate ephemeral key pair + +```javascript +request({ + method: 'eth_generateEphemeralKeyPair', + params: [version, signerPubKey], +}) +// expected return value: R +``` + +`signerPubKey` is optional. If provided, it is assumed that the implementation has the corresponding private key and the implementation MUST sign the ephemeral public key (in the form of what is to be returned). The signature algorithm is determined by the curve part of the `version` parameter, that is, ECDSA for secp256k1, and Ed25519 for Curve25519. And in this situation, it should be the case that *Sender* trusts `signerPubKey`, no matter how this trust is maintained. If not, the next request WILL be rejected by *Sender Application*. Also, see [Security Considerations](#security-considerations). + +The implementation then MUST generate random private key `r` with a cryptographic secure random number generator (CSRNG), and derive ephemeral public key `R = [r]G`. The implementation SHOULD keep the generated key pair `(r, R)` in a secure manner in accordance with the circumstances, and SHOULD keep it only for a limited duration, but the specific duration is left to individual implementations. The implementation SHOULD be able to retrieve `r` when given back the corresponding public key `R` if within the said duration. + +The return value is `R`, compressed if applicable. If `signerPubKey` is provided, then the `signature` is appended to `R`, also hex-encoded. + +Alternatively, `signature` could be calculated separately, and then appended to the returned data. + +#### R2. Request for Sender to encapsulate the private key + +```javascript +request({ + method: 'eth_encapsulatePrivateKey', + params: [ + version, + recipient, // public key, may be followed by its signature, see signerPubKey + signerPubKey, + oob, + salt, + account + ], +}) +// expected return value: S || cipher +``` + +`recipient` is the return value from the call to generate ephemeral key pair, with the optional `signature` appended either as returned or separately. + +`oob` and `salt` are just byte arrays. + +`account` is used to identify which private key to be encapsulated. With Ethereum, it is an address. Also, see [Encoding of data and messages](#encoding-of-data-and-messages). + +If `signerPubKey` is provided or `recipient` contains `signature` data, the implementation MUST perform signature verification. Missing data or incorrect format MUST either fail the call or result in an empty return and optional error logs. + +`signerPubKey` could have been further signed by another key pair `(trusted, trustedPubKey)`, which is trusted by *Sender Application*. In that case, `signerPubKey` is appended with the corresponding signature data, which SHOULD be verified against `trustedPubKey`. See [Test Cases](#test-cases) for further clarification. + +The implementation shall then proceed to retrieve the private key `sk` corresponding to `account`, and follow the [Core Algorithms](#core-algorithms) to encrypt it. + +The return data is a byte array that contains first *Sender*'s ephemeral public key `S` (compressed if applicable), then `cipher` including any authentication tag, that is, `S || cipher`. + +#### R3. Request for Recipient to unwrap and intake the private key + +```javascript +request({ + method: 'eth_intakePrivateKey', + params: [ + version, + recipientPublicKey, // no signature this time + oob, + salt, + data + ], +}) +// expected return value: account +``` + +This time `recipientPublicKey` is only the ephemeral public key `R` generated earlier in the Recipient side, just for the implementation to retrieve the corresponding private key `r`. `data` is the return value from the call to encapsulate private key, which is `S || cipher`. + +When the encapsulated private key `sk` is decrypted successfully, the implementation can process it further according to the designated purposes. Some general security guidelines SHALL be followed, for example, do *not* log the value, do securely wipe it after use, etc. + +The return value is the corresponding Ethereum address for `sk`, or empty if any error. + +### Options and Parameters + +Available elliptic curves are: + +- secp256k1 (mandatory) +- Curve25519 + +Available authenticated encryption schemes are: + +- AES-128-GCM (mandatory) +- AES-256-GCM +- Chacha20-Poly1305 + +The version string is simply the concatenation of the elliptic curve and AE scheme, for example, secp256k1-AES-128-GCM. The above lists allow a combination of six different concrete schemes. Implementations are encouraged to implement curve-related logic separately from authenticated encryption schemes to avoid duplication and to promote interoperability. + +Signature algorithms for each curve are: + +- secp256k1 --> ECDSA +- Curve25519 --> Ed25519 + +## Rationale + +A critical difference between this [EIP-6051](./eip-6051.md) with [EIP-5630](./eip-5630.md) is that, as the purpose of key encapsulation is to transport a private key securely, the public key from the key recipient should be ephemeral, and mostly used only one-time. While in EIP-5630 settings, the public key of the message recipient shall be stable for a while so that message senders can encrypt messages without key discovery every time. + +There is security implication to this difference, including perfect forward secrecy. We aim to achieve perfect forward secrecy by generating ephemeral key pairs on both sides every time: + +1) first *Recipient* shall generate an ephemeral key pair, retain the private key securely, and export the public key; +2) then *Sender* can securely wrap the private key in ECIES, with another ephemeral key pair, then destroy the ephemeral key securely; +3) finally *Recipient* can unwrap the private key, then destroy its ephemeral key pair securely. After these steps, the cipher text in transport intercepted by a malicious 3rd party is no longer decryptable. + +## Backwards Compatibility + +No backward compatibility issues for this new proposal. + +### Interoperability + +To minimize potential compatibility issues among applications (including hardware wallets), this EIP requires that version secp256k1-AES-128-GCM MUST be supported. + +The version could be decided by the user or negotiated by both sides. When there is no user input or negotiation, secp256k1-AES-128-GCM is assumed. + +It is expected that implementations cover curve supports separately from encryption support, that is, all the versions that could be derived from the supported curve and supported encryption scheme should work. + +Signatures to `R` and `signerPubKey` are applied to byte array values instead of the encoded string. + +### UX Recommendations + +`salt` and/or `oob` data: both are inputs to the HKDF function (`oob` as “info” parameter). For better user experiences we suggest to require from users only one of them but this is up to the implementation. + +*Recipient Application* is assumed to be powerful enough. *Sender Application* could have very limited computing power and user interaction capabilities. + +## Test Cases + +For review purposes, the program to generate the test vectors is open-sourced and provided in the corresponding discussion thread. + +### Data Fixation + +Throughout the test cases, we fix values for the below data: + +- `sk`, the private key to be encapsulated, fixed to: `0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315`. The corresponding address is `0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9`, called `account`. Note that these values come from the book *Mastering Ethereum* by Andreas M. Antonopoulos and Gavin Wood. +- `r`, the Recipient private key, fixed to `0x6f2dd2a7804705d2d536bee92221051865a639efa23f5ca7c810e77048253a79` +- `s`, the Sender private key, fixed to `0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8` +- `signer`, the private key to sign the ephemeral public key, fixed to `0xac304db075d1685284ba5e10c343f2324ee32df3394fc093c98932517d36e344`. When used for Ed25519 signing, however, this value acts as `seed`, while the actual private key is calculated as `SHA512(seed)[:32]`. Or put another way, the public key is the scalar multiplication of hashed private key to the base point. Same for `trusted`. +- `trusted`, the private key to sign `signerPubKey`, fixed to `0xda6649d68fc03b807e444e0034b3b59ec60716212007d72c9ddbfd33e25d38d1` +- `oob`, fixed to `0x313233343536` (string value: `123456`) +- `salt`, fixed to `0x6569703a2070726976617465206b657920656e63617073756c6174696f6e` (string value: `eip: private key encapsulation`) + +### Case 1 + +Use `version` as `secp256k1-AES-128-GCM`. **R1** is provided as: + +```javascript +request({ + method: 'eth_generateEphemeralKeyPair', + params: [ + version: 'secp256k1-AES-128-GCM', + signerPubKey: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b' + ], +}) +``` + +Suppose the implementation generates an ephemeral key pair `(r, R)`: + +``` +r: '0x6f2dd2a7804705d2d536bee92221051865a639efa23f5ca7c810e77048253a79', +R: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09' +``` + +The return value could be: + +``` +'0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09536da06b8d9207040ada179dc2c38f701a1a21c9ab5a7d52f5da50ea438e8ccf47dac77547fbdde194f71db52860b9e10ca2b089646f133d172124504ac1996a' +``` + +Note that `R` is compressed and `R` leads the return value: `R || sig`. + +Therefore **R2** could be provided as: + +```javascript +request({ + method: 'eth_encapsulatePrivateKey', + params: [ + version: 'secp256k1-AES-128-GCM', + recipient: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09536da06b8d9207040ada179dc2c38f701a1a21c9ab5a7d52f5da50ea438e8ccf47dac77547fbdde194f71db52860b9e10ca2b089646f133d172124504ac1996a', + signerPubKey: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b5bd427c527b7f1012b8edfd179b9002a7f2d7fc326bb6ae9aaf38b44eb93c397631fd8bb05fd78fa16ecca1eb19652b200f9048611265bc81f485cf60f29d6de', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9' + ], +}) +``` + +*Sender Application* first verifies first layer signature as ECDSA over secp256k1: + +``` +// actual message to be signed should be the decoded byte array +msg: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09', +sig: '0x536da06b8d9207040ada179dc2c38f701a1a21c9ab5a7d52f5da50ea438e8ccf47dac77547fbdde194f71db52860b9e10ca2b089646f133d172124504ac1996aaf4a811661741a43587dd458858b75c582ca7db82fa77b', +//signerPubKey +pub: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b' +``` + +Then it proceeds to verify the second layer signature, also as ECDSA over secp256k1: + +``` +// actual message to be signed should be the decoded byte array +msg: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b', +sig: '0x5bd427c527b7f1012b8edfd179b9002a7f2d7fc326bb6ae9aaf38b44eb93c397631fd8bb05fd78fa16ecca1eb19652b200f9048611265bc81f485cf60f29d6de', +//trustedPubKey +pub: '0x027fb72176f1f9852ce7dd9dc3aa4711675d3d8dc5102b86d758d853002137e839' +``` + +Since *Sender Application* trusts `trustedPubKey`, the signature verification succeeds. + +Suppose the implementation generates an ephemeral key pair `(s, S)` as: + +``` +s: '0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8', +S: '0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c02' +``` + +The shared secret, symmetric key, and IV should be: + +``` +ss: '0x8e83bc5a9c77b11afc12c9a8262b16e899678d1720459e3b73ca2abcfed1fca3', +skey: '0x6ccc02a61aa16d6c66a1277e5e2434b8', +IV: '0x9c7a0f870d17ced2d2c3d1cf' +``` + +Then the return value should be: + +``` +'0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c02abff407e8901bb37d13d724a2e3a8a1a5af300adc286aa2ec65ef2a38c10c5cec68a949d0a20dbad2a8e5dfd7a14bbcb' +``` + +With compressed public key `S` leading `cipher`, which in turn is (added prefix '0x'): + +``` +'0xabff407e8901bb37d13d724a2e3a8a1a5af300adc286aa2ec65ef2a38c10c5cec68a949d0a20dbad2a8e5dfd7a14bbcb' +``` + +Then **R3** is provided as: + +```javascript +request({ + method: 'eth_intakePrivateKey', + params: [ + version: 'secp256k1-AES-128-GCM', + recipientPublicKey: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + data: '0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c02abff407e8901bb37d13d724a2e3a8a1a5af300adc286aa2ec65ef2a38c10c5cec68a949d0a20dbad2a8e5dfd7a14bbcb' + ], +}) +``` + +The return value should be `0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9`. This matches the `account` parameter in **R2**. + +### Case 2 + +Use `version` as `secp256k1-AES-256-GCM`. The calculated symmetric key `skey`, `IV`, and `cipher` will be different. **R1** is provided as: + +```javascript +request({ + method: 'eth_generateEphemeralKeyPair', + params: [ + version: 'secp256k1-AES-256-GCM', + signerPubKey: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b' + ], +}) +``` + +Note that only the `version` is different (AES key size). We keep using the same `(r, R)` (this is just a test vector). + +Therefore **R2** is provided as: + +```javascript +request({ + method: 'eth_encapsulatePrivateKey', + params: [ + version: 'secp256k1-AES-256-GCM', + recipient: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09536da06b8d9207040ada179dc2c38f701a1a21c9ab5a7d52f5da50ea438e8ccf47dac77547fbdde194f71db52860b9e10ca2b089646f133d172124504ac1996a', + signerPubKey: '0x035a5ca16997f9b9ead9572c9bde36c5dab584b17bc965cdd7c2945c776e981b0b5bd427c527b7f1012b8edfd179b9002a7f2d7fc326bb6ae9aaf38b44eb93c397631fd8bb05fd78fa16ecca1eb19652b200f9048611265bc81f485cf60f29d6de', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9' + ], +}) +``` + +Suppose the implementation generates the same `(s, S)` as [Case 1](#case-1). The shared secret, symmetric key, and IV should be: + +``` +ss: '0x8e83bc5a9c77b11afc12c9a8262b16e899678d1720459e3b73ca2abcfed1fca3', +skey: '0x6ccc02a61aa16d6c66a1277e5e2434b89c7a0f870d17ced2d2c3d1cfd0e6f199', +IV: '0x3369b9570b9d207a0a8ebe27' +``` + +With shared secret `ss` remaining the same as [Case 1](#case-1), symmetric key `skey` contains both the `skey` and `IV` from [Case 1](#case-1). IV is changed. + +Then the return value should be the following, with the `S` part the same as [Case 1](#case-1) and the `cipher` part different: + +``` +'0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c0293910a91270b5deb0a645cc33604ed91668daf72328739d52a5af5a4760c4f3a9592b8f6d9b3ebe25127e7bf1c43b839' +``` + +Then **R3** is provided as: + +```javascript +request({ + method: 'eth_intakePrivateKey', + params: [ + version: 'secp256k1-AES-256-GCM', + recipientPublicKey: '0x039ef98feddb39664450c3876878093c70652caba7e3fd04333c0558ffdf798d09', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + data: '0x02ced2278d9ebb193f166d4ee5bbbc5ab8ca4b9ddf23c4172ad11185c079944c0293910a91270b5deb0a645cc33604ed91668daf72328739d52a5af5a4760c4f3a9592b8f6d9b3ebe25127e7bf1c43b839' + ], +}) +``` + +The return value should be `0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9`. This matches the `account` parameter in **R2**. + +### Case 3 + +Use `version` as: `Curve-25519-Chacha20-Poly1305`. **R1** is provided as: + +```javascript +request({ + method: 'eth_generateEphemeralKeyPair', + params: [ + version: 'Curve25519-Chacha20-Poly1305', + signerPubKey: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938' + ], +}) +``` + +Note that with Curve25519 the size is 32 (bytes) for both the public key and private key. And there is no compression for the public key. `signerPubKey` is calculated as: + +``` +//signer is '0xac304db075d1685284ba5e10c343f2324ee32df3394fc093c98932517d36e344' +s := SHA512(signer)[:32] +signerPubKey := Curve25519.ScalarBaseMult(s).ToHex() +``` + +The same technique applies to `trustedPubKey`. With `r` the same as in [Case 1](#case-1) and [Case 2](#case-2) and the curve being changed, the return value is `R = [r]G || sig`: + +``` +R = '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79cac06de279ec7f65f6b75f6bee740496df0650a6de61da5e691d7c5da1c7cb1ece61c669dd588a1029c38f11ad1714c1c9742232f9562ca6bbc7bad57882da04' +``` + +**R2** is provided as: + +```javascript +request({ + method: 'eth_encapsulatePrivateKey', + params: [ + version: 'Curve25519-Chacha20-Poly1305', + recipient: '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79879d900f04a955078ff6ae86f1d1b69b3e1265370e64bf064adaecb895c51effa3bdae7964bf8f9a6bfaef3b66306c1bc36afa5607a51b9768aa42ac2c961f02', + signerPubKey: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938d43e06a0f32c9e5ddb39fce34fac2b6f5314a1b1583134f27426d50af7094b0c101e848737e7f717da8c8497be06bab2a9536856c56eee194e89e94fd1bba509', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + account: '0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9' + ], +}) +``` + +Both `recipient` and `signerPubKey` have been signed in Ed25519. Verifying signature to `R` is carried out as: + +``` +// actual message to be signed should be the decoded byte array +msg: '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79', +sig: '0x879d900f04a955078ff6ae86f1d1b69b3e1265370e64bf064adaecb895c51effa3bdae7964bf8f9a6bfaef3b66306c1bc36afa5607a51b9768aa42ac2c961f02', +//signerPubKey +pub: '0xe509fb840f6d5a69333ef68d69b86de55b9b905e45b16e3591912c097ba69938' +``` + +After successfully verifying the signature (and the one by `trustedPubKey`), the implementation then generates ephemeral key pair `(s, S)` in Curve25519: + +``` +// s same as Case 1 and Case 2 +s = '0x28fa2db9f916e44fcc88370bedaf5eb3ec45632f040f4c1450c0f101e1e8bac8', +S = '0xd2fd6fcaac231d08363e736e61edb7e7696b13a727e3d2a239415cb8dc6ee278' +``` + +The shared secret, symmetric key, and IV should be: + +``` +ss: '0xe0b36f56cdb63c27e933a5a67a5e97db4b566c9276a36aeee5dc6e87da118867', +skey: '0x7c6fa749e6df13c8578dc44cb24cdf46a44cb163e1e570c2e590c720aed5783f', +IV: '0x3c98ef6fc34b0d6e7e16bd78' +``` + +Then the return value should be `S || cipher`: + +``` +'0xd2fd6fcaac231d08363e736e61edb7e7696b13a727e3d2a239415cb8dc6ee2786a7e2e40efb86dc68f44f3e032bbedb1259fa820e548ac5adbf191784c568d4f642ca5b60c0b2142189dff6ee464b95c' +``` + +Then **R3** is provided as: + +```javascript +request({ + method: 'eth_intakePrivateKey', + params: [ + version: 'Curve25519-Chacha20-Poly1305', + recipientPublicKey: '0xc0ea3514b0ab83b2fe4f4ef96159cda8fa836ce549ef09569b901eef0723bf79', + oob: '0x313233343536', + salt: '0x6569703a2070726976617465206b657920656e63617073756c6174696f6e', + data: '0xd2fd6fcaac231d08363e736e61edb7e7696b13a727e3d2a239415cb8dc6ee2786a7e2e40efb86dc68f44f3e032bbedb1259fa820e548ac5adbf191784c568d4f642ca5b60c0b2142189dff6ee464b95c' + ], +}) +``` + +The return value should be `0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9`. This matches the `account` parameter in **R2**. + +## Security Considerations + +### Perfect Forward Secrecy + +PFS is achieved by using ephemeral key pairs on both sides. + +### Optional Signature and Trusted Public Keys + +`R` could be signed so that *Sender Application* can verify if `R` could be trusted or not. This involves both signature verification and if the signer could be trusted or not. While signature verification is quite straightforward in itself, the latter should be managed with care. To facilitate this trust management issue, `signerPubKey` could be further signed, creating a dual-layer trust structure: + +``` +R <-- signerPubKey <-- trustedPubKey +``` + +This allows various strategies to manage trust. For example: + +- A hardware wallet vendor which takes it very seriously about the brand reputation and the fund safety for its customers, could choose to trust only its own public keys, all instances of `trustedPubKey`. These public keys only sign `signerPubKey` from selected partners. +- A MPC service could publish its `signerPubKey` online so that *Sender Application* won't verify the signature against a wrong or fake public key. + +Note that it is advised that a separate key pair should be used for signing on each curve. + +### Security Level + +1. We are not considering post-quantum security. If the quantum computer becomes a materialized threat, the underlying cipher of Ethereum and other L1 chains would have been replaced, and this EIP will be outdated then (as the EC part of ECIES is also broken). +2. The security level shall match that of the elliptic curve used by the underlying chains. It does not make much sense to use AES-256 to safeguard a secp256k1 private key but implementations could choose freely. +3. That being said, a key might be used in multiple chains. So the security level shall cover the most demanding requirement and potential future developments. + +AES-128, AES-256, and ChaCha20 are provided. + +### Randomness + +`r` and `s` must be generated with a cryptographic secure random number generator (CSRNG). + +`salt` could be random bytes generated the same way as `r` or `s`. `salt` could be in any length but the general suggestion is 12 or 16, which could be displayed as a QR code by the screen of some hardware wallet (so that another application could scan to read). If `salt` is not provided, this EIP uses the default value as `EIP-6051`. + +### Out of Band Data + +`oob` data is optional. When non-empty, its content is digits or an alpha-numeric string from the user. *Sender Application* may mandate `oob` from the user. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 0386f347448b73d4eecc5d3555de7f1da6223eae Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 27 Dec 2022 10:15:11 -0500 Subject: [PATCH 087/274] Update EIP-5380: Move to Review (#6217) --- EIPS/eip-5380.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5380.md b/EIPS/eip-5380.md index 32e996fc3e6c39..1da37a40cf1650 100644 --- a/EIPS/eip-5380.md +++ b/EIPS/eip-5380.md @@ -4,7 +4,7 @@ title: EIP-721 Entitlement Extension description: Allows token owners to grant the ability for others to use specific properties of those tokens author: Pandapip1 (@Pandapip1), Tim Daubenschütz (@TimDaub) discussions-to: https://ethereum-magicians.org/t/pr-5380-eip-4907-alternative-design/10190 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-03-11 From 732793d584b023adec15c5126829016b91e04753 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 27 Dec 2022 12:57:03 -0500 Subject: [PATCH 088/274] Update EIP-6059: Fix multi-line list elements (#6223) --- EIPS/eip-6059.md | 63 ++++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 45 deletions(-) diff --git a/EIPS/eip-6059.md b/EIPS/eip-6059.md index 2a1430bf4165ae..ebafebe5ef4fd2 100644 --- a/EIPS/eip-6059.md +++ b/EIPS/eip-6059.md @@ -371,40 +371,23 @@ ID MUST never be a `0` value, as this proposal uses `0` values do signify that t Designing the proposal, we considered the following questions: -1. **How to name the proposal?** - +1. **How to name the proposal?**\ In an effort to provide as much information about the proposal we identified the most important aspect of the proposal; the parent centered control over nesting. The child token's role is only to be able to be `Nestable` and support a token owning it. This is how we landed on the `Parent-Centered` part of the title. - -2. **Why is automatically accepting a child using [EIP-712](./eip-712.md) permit-style signatures not a part of this proposal?** - +2. **Why is automatically accepting a child using [EIP-712](./eip-712.md) permit-style signatures not a part of this proposal?**\ For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with assets. - -3. **Why use indexes?** - -To reduce the gas consumption. If the token ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. Lists of active and pending children per token need to be maintained, since methods to get them are part of the proposed interface. - -To avoid race conditions in which the index of a token changes, the expected token ID as well as the expected token's collection smart contract is included in operations requiring token index, to verify that the token being accessed using the index is the expected one. - +3. **Why use indexes?**\ +To reduce the gas consumption. If the token ID was used to find which token to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending children arrays. With the index, the cost is fixed. Lists of active and pending children per token need to be maintained, since methods to get them are part of the proposed interface.\ +To avoid race conditions in which the index of a token changes, the expected token ID as well as the expected token's collection smart contract is included in operations requiring token index, to verify that the token being accessed using the index is the expected one.\ Implementation that would internally keep track of indices using mapping was attempted. The minimum cost of accepting a child token was increased by over 20% and the cost of minting has increased by over 15%. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept the increased transaction cost this incurs. In the sample implementation provided, there are several hooks which make this possible. - -4. **Why is the pending children array limited instead of supporting pagination?** - -The pending child tokens array is not meant to be a buffer to collect the tokens that the root owner of the parent token wants to keep, but not enough to promote them to active children. It is meant to be an easily traversable list of child token candidates and should be regularly maintained; by either accepting or rejecting proposed child tokens. There is also no need for the pending child tokens array to be unbounded, because active child tokens array is. - -Another benefit of having bounded child tokens array is to guard against spam and griefing. As minting malicious or spam tokens could be relatively easy and low-cost, the bounded pending array assures that all of the tokens in it are easy to identify and that legitimate tokens are not lost in a flood of spam tokens, if one occurs. - +4. **Why is the pending children array limited instead of supporting pagination?**\ +The pending child tokens array is not meant to be a buffer to collect the tokens that the root owner of the parent token wants to keep, but not enough to promote them to active children. It is meant to be an easily traversable list of child token candidates and should be regularly maintained; by either accepting or rejecting proposed child tokens. There is also no need for the pending child tokens array to be unbounded, because active child tokens array is.\ +Another benefit of having bounded child tokens array is to guard against spam and griefing. As minting malicious or spam tokens could be relatively easy and low-cost, the bounded pending array assures that all of the tokens in it are easy to identify and that legitimate tokens are not lost in a flood of spam tokens, if one occurs.\ A consideration tied to this issue was also how to make sure, that a legitimate token is not accidentally rejected when clearing the pending child tokens array. We added the maximum pending children to reject argument to the clear pending child tokens array call. This assures that only the intended number of pending child tokens is rejected and if a new token is added to the pending child tokens array during the course of preparing such call and executing it, the clearing of this array SHOULD result in a reverted transaction. - -5. **Should we allow tokens to be nested into one of its children?** - +5. **Should we allow tokens to be nested into one of its children?**\ The proposal enforces that a parent token can't be nested into one of its child token, or downstream child tokens for that matter. A parent token and its children are all managed by the parent token's root owner. This means that if a token would be nested into one of its children, this would create the ownership loop and none of the tokens within the loop could be managed anymore. - -6. **Why is there not a "safe" nest transfer method?** - +6. **Why is there not a "safe" nest transfer method?**\ `nestTransfer` is always "safe" since it MUST check for `INestable` compatibility on the destination. - -7. **How does this proposal differ from the other proposals trying to address a similar problem?** - +7. **How does this proposal differ from the other proposals trying to address a similar problem?**\ This interface allows for tokens to both be sent to and receive other tokens. The propose-accept and parent governed patterns allow for a more secure use. The backward compatibility is only added for EIP-721, allowing for a simpler interface. The proposal also allows for different collections to inter-operate, meaning that nesting is not locked to a single smart contract, but can be executed between completely separate NFT collections. ### Propose-Commit pattern for child token management @@ -454,26 +437,16 @@ To better understand how these state transitions are achieved, we have to look a Based on the desired state transitions, the values of these parameters have to be set accordingly (any parameters not set in the following examples depend on the child token being managed): -1. **Reject child token** - +1. **Reject child token**\ ![Reject child token](../assets/eip-6059/img/eip-6059-reject-child.png) - -2. **Abandon child token** - +2. **Abandon child token**\ ![Abandon child token](../assets/eip-6059/img/eip-6059-abandon-child.png) - -3. **Unnest child token** - -![Unnest child token](../assets/eip-6059/img/eip-6059-unnest-child.png) - -4. **Transfer the child token to an EOA or an `ERC721Receiver`** - +3. **Unnest child token**\ +![Unnest child token](../assets/eip-6059/img/eip-6059-unnest-child.png)\ +4. **Transfer the child token to an EOA or an `ERC721Receiver`**\ ![Transfer child token to EOA](../assets/eip-6059/img/eip-6059-transfer-child-to-eoa.png) - -5. **Transfer the child token into a new parent token** - -![Transfer child token to parent token](../assets/eip-6059/img/eip-6059-transfer-child-to-token.png) - +5. **Transfer the child token into a new parent token**\ +![Transfer child token to parent token](../assets/eip-6059/img/eip-6059-transfer-child-to-token.png)\ This state change places the token in the pending array of the new parent token. The child token still needs to be accepted by the new parent token's root owner in order to be placed into the active array of that token. ## Backwards Compatibility From 3419568ae831ab6a07de8713c2a34eeaacc975d3 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Tue, 27 Dec 2022 19:13:44 +0100 Subject: [PATCH 089/274] Fix formatting of ordered list in Rationale (#6225) --- EIPS/eip-5773.md | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/EIPS/eip-5773.md b/EIPS/eip-5773.md index 23114adaa37b6e..43918b3f44db52 100644 --- a/EIPS/eip-5773.md +++ b/EIPS/eip-5773.md @@ -425,39 +425,24 @@ The optional properties of the metadata JSON MAY include the following fields, o Designing the proposal, we considered the following questions: -1. **Should we use Asset or Resource when referring to the structure that comprises the token?** - -The original idea was to call the proposal Multi-Resource, but while this denoted the broadness of the structures that could be held by a single token, the term *asset* represents it better. - +1. **Should we use Asset or Resource when referring to the structure that comprises the token?**\ +The original idea was to call the proposal Multi-Resource, but while this denoted the broadness of the structures that could be held by a single token, the term *asset* represents it better.\ An asset is defined as something that is owned by a person, company, or organization, such as money, property, or land. This is the best representation of what an asset of this proposal can be. An asset in this proposal can be a multimedia file, technical information, a land deed, or anything that the implementer has decided to be an asset of the token they are implementing. - -2. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?** - +2. **Why are [EIP-712](./eip-712.md) permit-style signatures to manage approvals not used?**\ For consistency. This proposal extends EIP-721 which already uses 1 transaction for approving operations with tokens. It would be inconsistent to have this and also support signing messages for operations with assets. - -3. **Why use indexes?** - -To reduce the gas consumption. If the asset ID was used to find which asset to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending assets arrays. With the index, the cost is fixed. A list of active and pending assets arrays per token need to be maintained, since methods to get them are part of the proposed interface. - -To avoid race conditions in which the index of an asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset. - +3. **Why use indexes?**\ +To reduce the gas consumption. If the asset ID was used to find which asset to accept or reject, iteration over arrays would be required and the cost of the operation would depend on the size of the active or pending assets arrays. With the index, the cost is fixed. A list of active and pending assets arrays per token need to be maintained, since methods to get them are part of the proposed interface.\ +To avoid race conditions in which the index of an asset changes, the expected asset ID is included in operations requiring asset index, to verify that the asset being accessed using the index is the expected asset.\ Implementation that would internally keep track of indices using mapping was attempted. The average cost of adding an asset to a token increased by over 25%, costs of accepting and rejecting assets also increased 4.6% and 7.1% respectively. We concluded that it is not necessary for this proposal and can be implemented as an extension for use cases willing to accept this cost. In the sample implementation provided, there are several hooks which make this possible. - -4. **Why is a method to get all the assets not included?** - +4. **Why is a method to get all the assets not included?**\ Getting all assets might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. - -5. **Why is pagination not included?** - +5. **Why is pagination not included?**\ Asset IDs use `uint64`, testing has confirmed that the limit of IDs you can read before reaching the gas limit is around 30.000. This is not expected to be a common use case so it is not a part of the interface. However, an implementer can create an extension for this use case if needed. - -6. **How does this proposal differ from the other proposals trying to address a similar problem?** - +6. **How does this proposal differ from the other proposals trying to address a similar problem?**\ After reviewing them, we concluded that each contains at least one of these limitations: - -- Using a single URI which is replaced as new assets are needed, this introduces a trust issue for the token owner. -- Focusing only on a type of asset, while this proposal is asset type agnostic. -- Having a different token for each new use case, this means that the token is not forward-compatible. + - Using a single URI which is replaced as new assets are needed, this introduces a trust issue for the token owner. + - Focusing only on a type of asset, while this proposal is asset type agnostic. + - Having a different token for each new use case, this means that the token is not forward-compatible. ### Multi-Asset Storage Schema From 6ea1ff17507188bf0283081f1287fdfff74984d5 Mon Sep 17 00:00:00 2001 From: Ori Pomerantz Date: Wed, 28 Dec 2022 00:22:46 -0600 Subject: [PATCH 090/274] Updated EIP 3540's data section to new format (#6222) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated EIP 3540's data section to new format * Update EIPS/eip-3540.md Co-authored-by: Paweł Bylica Co-authored-by: Paweł Bylica --- EIPS/eip-3540.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 951492eaf5a37a..5a1da1e40d80b1 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -238,10 +238,10 @@ We have considered different questions for the sections: ### Data-only contracts -The EOF prevents deploying contracts with arbitrary bytes (data-only contracts: their purpose is to store data not execution). **EOF1 requires** presence of a **code section** therefore the minimal overhead EOF data contract consist of a data section and one code section with single instruction. We recommend to use `INVALID` instruction in this case. In total there are 11 additional bytes required. +The EOF prevents deploying contracts with arbitrary bytes (data-only contracts: their purpose is to store data not execution). **EOF1 requires** presence of a **code section** therefore the minimal overhead EOF data contract consist of a data section and one code section with single instruction. We recommend to use `INVALID` instruction in this case. In total there are 20 additional bytes required. ``` -EF0001 010001 02 00 FE +EF0001 010004 020001 0001 03 00 00000000 FE ``` It is possible in the future that this data will be accessible with data-specific opcodes, such as `DATACOPY` or `EXTDATACOPY`. Until then, callers will need to determine the data offset manually. From f6c5558bc9a2841ab2d9811ffee7702b14f82209 Mon Sep 17 00:00:00 2001 From: Sam Porter <79773335+SamPorter1984@users.noreply.github.com> Date: Wed, 28 Dec 2022 14:02:45 +0300 Subject: [PATCH 091/274] Update eip-3561.md (#6230) * Update eip-3561.md * Update eip-3561.md * Update eip-3561.md --- EIPS/eip-3561.md | 374 +++++++++++++++++++++++++++-------------------- 1 file changed, 212 insertions(+), 162 deletions(-) diff --git a/EIPS/eip-3561.md b/EIPS/eip-3561.md index d2f17744f291f3..f3fbee881717ab 100644 --- a/EIPS/eip-3561.md +++ b/EIPS/eip-3561.md @@ -11,10 +11,12 @@ created: 2021-05-09 --- ## Abstract -Removing trust from upgradeability proxy is required for anonymous developers. To achieve that, disallowing instant, potentially malicious upgrades is required. This EIP introduces additional storage slots for upgradeability proxy which are assumed to decrease trust in interaction with upgradeable smart contracts. Defined by the admin implementation becomes an active implementation only after Zero Trust Period allows. + +Removing trust from upgradeability proxy is necessary for anonymous developers. In order to accomplish this, instant and potentially malicious upgrades must be prevented. This EIP introduces additional storage slots for upgradeability proxy which are assumed to decrease trust in interaction with upgradeable smart contracts. Defined by the admin implementation logic can be made an active implementation logic only after Zero Trust Period allows. ## Motivation -It's usually not possible for anonymous developers who uses upgradeability proxies to gain community trust. + +Anonymous developers who utilize upgradeability proxies typically struggle to earn the trust of the community. Fairer, better future for humanity absolutely requires some developers to stay anonymous while still attract vital attention to solutions they propose and at the same time leverage the benefits of possible upgradeability. @@ -24,17 +26,19 @@ The specification is an addition to the standard [EIP-1967](./eip-1967.md) trans The specification focuses on the slots it adds. All admin interactions with trust minimized proxy must emit an event to make admin actions trackable, and all admin actions must be guarded with `onlyAdmin()` modifier. ### Next Logic Contract Address + Storage slot `0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685` (obtained as `bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1)`). -Logic address must be first defined as next logic, before it can function as actual logic implementation stored in EIP-1967 `IMPLEMENTATION_SLOT`. +Desirable implementation logic address must be first defined as next logic, before it can function as actual logic implementation stored in EIP-1967 `IMPLEMENTATION_SLOT`. Admin interactions with next logic contract address correspond with these methods and events: + ```solidity -// sets next logic contract address and initializes it. Emits NextLogicDefined -// 0x as calldata is an equivalent of proposeTo() -// calldata is ignored here if zero trust period, described below, was already set -function proposeTo(address implementation, bytes calldata data) external onlyAdmin; -// sets the address stored as next implementation as current IMPLEMENTATION_SLOT -// as soon UPGRADE_BLOCK_SLOT allows -function upgrade() external onlyAdmin; +// Sets next logic contract address. Emits NextLogicDefined +// If current implementation is address(0), then upgrades to IMPLEMENTATION_SLOT +// immedeatelly, therefore takes data as an argument +function proposeTo(address implementation, bytes calldata data) external IfAdmin +// As soon UPGRADE_BLOCK_SLOT allows, sets the address stored as next implementation +// as current IMPLEMENTATION_SLOT and initializes it. +function upgrade(bytes calldata data) external IfAdmin // cancelling is possible for as long as upgrade() for given next logic was not called // emits NextLogicCanceled function cancelUpgrade() external onlyAdmin; @@ -44,189 +48,235 @@ event NextLogicCanceled(address indexed oldLogic); ``` ### Upgrade Block + Storage slot `0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee` (obtained as `bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1)`). -On/after this block next logic contract address can be set to EIP-1967 `IMPLEMENTATION_SLOT` or, in other words, start to function as current logic. Updated automatically according to Zero Trust Period, shown as `earliestArrivalBlock` in the event `NextLogicDefined`. +On/after this block next logic contract address can be set to EIP-1967 `IMPLEMENTATION_SLOT` or, in other words, `upgrade()` can be called. Updated automatically according to Zero Trust Period, shown as `earliestArrivalBlock` in the event `NextLogicDefined`. ### Propose Block + Storage slot `0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388` (obtained as `bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1)`). Defines after/on which block *proposing* next logic is possible. Required for convenience, for example can be manually set to a year from given time. Can be set to maximum number to completely seal the code. Admin interactions with this slot correspond with this method and event: + ```solidity function prolongLock(uint b) external onlyAdmin; event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival); ``` ### Zero Trust Period + Storage slot `0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720` (obtained as `bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1)`). Zero Trust Period in amount of blocks, can only be set higher than previous value. While it is at default value(0), the proxy operates exactly as standard EIP-1967 transparent proxy. After zero trust period is set, all above specification is enforced. Admin interactions with this slot should correspond with this method and event: + ```solidity function setZeroTrustPeriod(uint blocks) external onlyAdmin; event ZeroTrustPeriodSet(uint blocks); ``` + ### Implementation Example + ```solidity -pragma solidity >=0.8.0;//important +pragma solidity >=0.8.0; //important // EIP-3561 trust minimized proxy implementation https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3561.md - -contract TrustMinimizedProxy{ - event Upgraded(address indexed toLogic); - event AdminChanged(address indexed previousAdmin, address indexed newAdmin); - event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock); - event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival); - event NextLogicCanceled(); - event ZeroTrustPeriodSet(uint blocks); - - bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - bytes32 internal constant LOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - bytes32 internal constant NEXT_LOGIC_SLOT = 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685; - bytes32 internal constant NEXT_LOGIC_BLOCK_SLOT = 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee; - bytes32 internal constant PROPOSE_BLOCK_SLOT = 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388; - bytes32 internal constant ZERO_TRUST_PERIOD_SLOT = 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720; - - constructor() payable { - require(ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) && LOGIC_SLOT==bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) - && NEXT_LOGIC_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1) && NEXT_LOGIC_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1) - && PROPOSE_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1) && ZERO_TRUST_PERIOD_SLOT == bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1)); - _setAdmin(msg.sender); - } - - modifier ifAdmin() { - if (msg.sender == _admin()) { - _; - } else { - _fallback(); - } - } - - function _logic() internal view returns (address logic) { - assembly { logic := sload(LOGIC_SLOT) } - } - - function _nextLogic() internal view returns (address nextLogic) { - assembly { nextLogic := sload(NEXT_LOGIC_SLOT) } - } - - function _proposeBlock() internal view returns (uint bl) { - assembly { bl := sload(PROPOSE_BLOCK_SLOT) } - } - - function _nextLogicBlock() internal view returns (uint bl) { - assembly { bl := sload(NEXT_LOGIC_BLOCK_SLOT) } - } - - function _zeroTrustPeriod() internal view returns (uint tm) { - assembly { tm := sload(ZERO_TRUST_PERIOD_SLOT) } - } - - function _admin() internal view returns (address adm) { - assembly { adm := sload(ADMIN_SLOT) } - } - - function _setAdmin(address newAdm) internal { - assembly { sstore(ADMIN_SLOT, newAdm) } - } - - function changeAdmin(address newAdm) external ifAdmin { - emit AdminChanged(_admin(), newAdm); - _setAdmin(newAdm); - } - - function upgrade(bytes calldata data) external ifAdmin { - require(block.number>=_nextLogicBlock(),"too soon"); - address logic; - assembly { - logic := sload(NEXT_LOGIC_SLOT) - sstore(LOGIC_SLOT,logic) - } - (bool success,) = logic.delegatecall(data); - require(success,"failed to call"); - emit Upgraded(logic); - } - - fallback () external payable { - _fallback(); - } - - receive () external payable { - _fallback(); - } - - function _fallback() internal { - require(msg.sender != _admin()); - _delegate(_logic()); - } - - function cancelUpgrade() external ifAdmin { - address logic; - assembly { - logic := sload(LOGIC_SLOT) - sstore(NEXT_LOGIC_SLOT, logic) - } - emit NextLogicCanceled(); - } - - function prolongLock(uint b) external ifAdmin { - require(b > _proposeBlock(),"get maxxed"); - assembly {sstore(PROPOSE_BLOCK_SLOT,b)} - emit ProposingUpgradesRestrictedUntil(b,b+_zeroTrustPeriod()); - } - - function setZeroTrustPeriod(uint blocks) external ifAdmin { // before this set at least once acts like a normal eip 1967 transparent proxy - uint ztp; - assembly { ztp := sload(ZERO_TRUST_PERIOD_SLOT) } - require(blocks>ztp,"can be only set higher"); - assembly{ sstore(ZERO_TRUST_PERIOD_SLOT, blocks) } - emit ZeroTrustPeriodSet(blocks); - } - - function _updateBlockSlot() internal { - uint nlb = block.number + _zeroTrustPeriod(); - assembly {sstore(NEXT_LOGIC_BLOCK_SLOT,nlb)} - } - - function _setNextLogic(address nl) internal { - require(block.number >= _proposeBlock(),"too soon"); - _updateBlockSlot(); - assembly { sstore(NEXT_LOGIC_SLOT, nl)} - emit NextLogicDefined(nl,block.number + _zeroTrustPeriod()); - } - - function proposeTo(address newLogic, bytes calldata data) payable external ifAdmin { - if (_zeroTrustPeriod() == 0) { - _updateBlockSlot(); - assembly {sstore(LOGIC_SLOT,newLogic)} - (bool success,) = newLogic.delegatecall(data); - require(success,"failed to call"); - emit Upgraded(newLogic); - } else{ - _setNextLogic(newLogic); - } - } - - function _delegate(address logic_) internal { - assembly { - calldatacopy(0, 0, calldatasize()) - let result := delegatecall(gas(), logic_, 0, calldatasize(), 0, 0) - returndatacopy(0, 0, returndatasize()) - switch result - case 0 { revert(0, returndatasize()) } - default { return(0, returndatasize()) } - } - } +// Based on EIP-1967 upgradeability proxy: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1967.md + +contract TrustMinimizedProxy { + event Upgraded(address indexed toLogic); + event AdminChanged(address indexed previousAdmin, address indexed newAdmin); + event NextLogicDefined(address indexed nextLogic, uint earliestArrivalBlock); + event ProposingUpgradesRestrictedUntil(uint block, uint nextProposedLogicEarliestArrival); + event NextLogicCanceled(); + event ZeroTrustPeriodSet(uint blocks); + + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + bytes32 internal constant LOGIC_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + bytes32 internal constant NEXT_LOGIC_SLOT = 0x19e3fabe07b65998b604369d85524946766191ac9434b39e27c424c976493685; + bytes32 internal constant NEXT_LOGIC_BLOCK_SLOT = 0xe3228ec3416340815a9ca41bfee1103c47feb764b4f0f4412f5d92df539fe0ee; + bytes32 internal constant PROPOSE_BLOCK_SLOT = 0x4b50776e56454fad8a52805daac1d9fd77ef59e4f1a053c342aaae5568af1388; + bytes32 internal constant ZERO_TRUST_PERIOD_SLOT = 0x7913203adedf5aca5386654362047f05edbd30729ae4b0351441c46289146720; + + constructor() payable { + require( + ADMIN_SLOT == bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1) && + LOGIC_SLOT == bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) && + NEXT_LOGIC_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic')) - 1) && + NEXT_LOGIC_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.next.logic.block')) - 1) && + PROPOSE_BLOCK_SLOT == bytes32(uint256(keccak256('eip3561.proxy.propose.block')) - 1) && + ZERO_TRUST_PERIOD_SLOT == bytes32(uint256(keccak256('eip3561.proxy.zero.trust.period')) - 1) + ); + _setAdmin(msg.sender); + } + + modifier IfAdmin() { + if (msg.sender == _admin()) { + _; + } else { + _fallback(); + } + } + + function _logic() internal view returns (address logic) { + assembly { + logic := sload(LOGIC_SLOT) + } + } + + function _nextLogic() internal view returns (address nextLogic) { + assembly { + nextLogic := sload(NEXT_LOGIC_SLOT) + } + } + + function _proposeBlock() internal view returns (uint b) { + assembly { + b := sload(PROPOSE_BLOCK_SLOT) + } + } + + function _nextLogicBlock() internal view returns (uint b) { + assembly { + b := sload(NEXT_LOGIC_BLOCK_SLOT) + } + } + + function _zeroTrustPeriod() internal view returns (uint ztp) { + assembly { + ztp := sload(ZERO_TRUST_PERIOD_SLOT) + } + } + + function _admin() internal view returns (address adm) { + assembly { + adm := sload(ADMIN_SLOT) + } + } + + function _setAdmin(address newAdm) internal { + assembly { + sstore(ADMIN_SLOT, newAdm) + } + } + + function changeAdmin(address newAdm) external IfAdmin { + emit AdminChanged(_admin(), newAdm); + _setAdmin(newAdm); + } + + function upgrade(bytes calldata data) external IfAdmin { + require(block.number >= _nextLogicBlock(), 'too soon'); + address logic; + assembly { + logic := sload(NEXT_LOGIC_SLOT) + sstore(LOGIC_SLOT, logic) + } + (bool success, ) = logic.delegatecall(data); + require(success, 'failed to call'); + emit Upgraded(logic); + } + + fallback() external payable { + _fallback(); + } + + receive() external payable { + _fallback(); + } + + function _fallback() internal { + require(msg.sender != _admin()); + _delegate(_logic()); + } + + function cancelUpgrade() external IfAdmin { + address logic; + assembly { + logic := sload(LOGIC_SLOT) + sstore(NEXT_LOGIC_SLOT, logic) + } + emit NextLogicCanceled(); + } + + function prolongLock(uint b) external IfAdmin { + require(b > _proposeBlock(), 'can be only set higher'); + assembly { + sstore(PROPOSE_BLOCK_SLOT, b) + } + emit ProposingUpgradesRestrictedUntil(b, b + _zeroTrustPeriod()); + } + + function setZeroTrustPeriod(uint blocks) external IfAdmin { + // before this set at least once acts like a normal eip 1967 transparent proxy + uint ztp; + assembly { + ztp := sload(ZERO_TRUST_PERIOD_SLOT) + } + require(blocks > ztp, 'can be only set higher'); + assembly { + sstore(ZERO_TRUST_PERIOD_SLOT, blocks) + } + _updateNextBlockSlot(); + emit ZeroTrustPeriodSet(blocks); + } + + function _updateNextBlockSlot() internal { + uint nlb = block.number + _zeroTrustPeriod(); + assembly { + sstore(NEXT_LOGIC_BLOCK_SLOT, nlb) + } + } + + function _setNextLogic(address nl) internal { + require(block.number >= _proposeBlock(), 'too soon'); + _updateNextBlockSlot(); + assembly { + sstore(NEXT_LOGIC_SLOT, nl) + } + emit NextLogicDefined(nl, block.number + _zeroTrustPeriod()); + } + + function proposeTo(address newLogic, bytes calldata data) external payable IfAdmin { + if (_zeroTrustPeriod() == 0 || _logic() == address(0)) { + _updateNextBlockSlot(); + assembly { + sstore(LOGIC_SLOT, newLogic) + } + (bool success, ) = newLogic.delegatecall(data); + require(success, 'failed to call'); + emit Upgraded(newLogic); + } else { + _setNextLogic(newLogic); + } + } + + function _delegate(address logic_) internal { + assembly { + calldatacopy(0, 0, calldatasize()) + let result := delegatecall(gas(), logic_, 0, calldatasize(), 0, 0) + returndatacopy(0, 0, returndatasize()) + switch result + case 0 { + revert(0, returndatasize()) + } + default { + return(0, returndatasize()) + } + } + } } ``` ## Rationale -An argument "just don't make such contracts upgadeable at all" fails when it comes to complex systems which do or do not heavily reliant on human factor which might manifest itself in unprecedented ways. It might be impossible to model some systems right on first try. Using decentralized governance for upgrade management coupled with EIP-1967 proxy could also become a serious bottleneck for certain protocols before they mature and data is at hand. -A proxy without a time delay before an actual upgrade is obviously abusable. A time delay is probably unavoidable, even if it means that inexperienced developers might not have confidence using it. Albeit this is a downside of this EIP, it's a critically important option to have in smart contract development today. +An argument "just don't make such contracts upgadeable at all" fails when it comes to complex systems which do or do not heavily rely on human factor, which might manifest itself in unprecedented ways. It might be impossible to model some systems right on first try. Using decentralized governance for upgrade management coupled with EIP-1967 proxy might become a serious bottleneck for certain protocols before they mature and data is at hand. -Propose block adds to convenience if used, so should be kept. +A proxy without a time delay before an actual upgrade is obviously abusable. A time delay is probably unavoidable, even if it means that inexperienced developers might not have confidence using it. Albeit this is a downside of this EIP, it's a critically important option to have in smart contract development today. ## Security Considerations -Users must ensure that a trust-minimized proxy they interact with does not allow overflows, ideally represents the exact copy of the code in implementation example above, and also they must ensure that Zero Trust Period length is reasonable(at the very least two weeks if finalized upgrades are usually being revealed beforehand, and in most cases at least a month). + +Users must ensure that a trust-minimized proxy they interact with does not allow overflows, ideally represents the exact copy of the code in implementation example above, and also they must ensure that Zero Trust Period length is reasonable(at the very least two weeks if upgrades are usually being revealed beforehand, and in most cases at least a month). ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From a53796fb48328ec3c491e1222b6e707801e4fe87 Mon Sep 17 00:00:00 2001 From: Ori Pomerantz Date: Wed, 28 Dec 2022 14:36:21 -0600 Subject: [PATCH 092/274] Explained typesize in the v.1 header (#6232) Also, fixed the minimum (0003 -> 0004) and maximum (FFFF -> FFFC) because it has to be divisible by four --- EIPS/eip-3540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 5a1da1e40d80b1..ef9e1997a7d189 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -123,7 +123,7 @@ type_section := (inputs, outputs, max_stack_height)+ | magic | 2 bytes | 0xEF00 | EOF prefix | | version | 1 byte | 0x01 | EOF version | | kind_type | 1 byte | 0x01 | kind marker for EIP-4750 type section header | -| type_size | 2 bytes | 0x0003-0xFFFF | uint16 denoting the length of the type section content | +| type_size | 2 bytes | 0x0004-0xFFFC | uint16 denoting the length of the type section content, 4 bytes per code segment | | kind_code | 1 byte | 0x02 | kind marker for code size section | | num_code_sections | 2 bytes | 0x0001-0xFFFF | uint16 denoting the number of the code sections | | code_size | 2 bytes | 0x0001-0xFFFF | uint16 denoting the length of the code section content | From e6d574be6c45c79750f559bb5f8247e9d37c46f1 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Thu, 29 Dec 2022 18:19:18 +0200 Subject: [PATCH 093/274] Update EIP-4337 to latest working version (#6233) * Update to latest working version Update the EIP to the working version from https://github.com/eth-infinitism/account-abstraction/blob/develop/eip/EIPS/eip-4337.md Changes: AA-94 update keccak rules. AA-93 Adding debug RPC APIs for the Bundler to use (#153) AA 92 simulate execution (#152) AA 73 unify reputation (#144) AA-68 rpc calls (#132) AA-61 rename wallet to account (#134) AA-69 wallet support for simulation without signing (#133) AA-70 rename requestId to userOpHash (#138) AA-67 relax storage rules in opcode banning (#121) AA-63 remove paymaster stake value from EntryPoint (#119) AA-51 simpler simulation api, including aggregation AA-60 validate timestamp (#117) Clarify wallet factory behavior when the wallet already exists (#118) * lint fixes --- EIPS/eip-4337.md | 741 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 584 insertions(+), 157 deletions(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index 474d9fae853805..da3ef3156e5d73 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -1,6 +1,6 @@ --- eip: 4337 -title: Account Abstraction using alt mempool +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 @@ -12,7 +12,7 @@ created: 2021-09-29 ## Abstract -An account abstraction proposal which completely avoids the need for consensus-layer protocol changes. Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal instead introduces a higher-layer pseudo-transaction object called a `UserOperation`. Users send `UserOperation` objects into a separate mempool. A special class of actor called bundlers (either miners, or users that can send transactions to miners through a bundle marketplace) package up a set of these objects into a transaction making a `handleOps` call to a special contract, and that transaction then gets included in a block. +An account abstraction proposal which completely avoids the need for consensus-layer protocol changes. Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal instead introduces a higher-layer pseudo-transaction object called a `UserOperation`. Users send `UserOperation` objects into a separate mempool. A special class of actor called bundlers (either block builders, or users that can send transactions to block builders through a bundle marketplace) package up a set of these objects into a transaction making a `handleOps` call to a special contract, and that transaction then gets included in a block. ## Motivation @@ -22,13 +22,13 @@ This proposal takes a different approach, avoiding any adjustments to the consen * **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 + * Allow any bundler (think: block builder) 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 * Avoid trust assumptions on bundlers * **Do not require any Ethereum consensus changes**: Ethereum consensus layer development is focusing on the merge and later on scalability-oriented features, and there may not be any opportunity for further protocol changes for a long time. Hence, to increase the chance of faster adoption, this proposal avoids Ethereum consensus changes. * **Try to support other use cases** * Privacy-preserving applications - * Atomic multi-operations (similar goal to EIP-3074) + * Atomic multi-operations (similar goal to [EIP-3074](./eip-3074.md)) * 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) @@ -42,15 +42,17 @@ This proposal takes a different approach, avoiding any adjustments to the consen * 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. +* **Bundler** - a node (block builder) that bundles multiple UserOperations and create an EntryPoint.handleOps() transaction. Note that not all block-builders on the network are required to be bundlers +* **Aggregator** - a helper contract trusted by accounts 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`: + +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 account 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 (needed if and only if the wallet is not yet on-chain and needs to be created) | +| `sender` | `address` | The account making the operation | +| `nonce` | `uint256` | Anti-replay parameter; also used as the salt for first-time account creation | +| `initCode` | `bytes` | The initCode of the account (needed if and only if the account is not yet on-chain and needs to be created) | | `callData` | `bytes` | The data to pass to the `sender` during the main execution call | | `callGasLimit` | `uint256` | The amount of gas to allocate the main execution call | | `verificationGasLimit` | `uint256` | The amount of gas to allocate for the verification step | @@ -58,15 +60,15 @@ To avoid Ethereum consensus changes, we do not attempt to create new transaction | `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 | +| `signature` | `bytes` | Data passed into the account 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**. +Users send `UserOperation` objects to a dedicated user operation mempool. A specialized class of actors called **bundlers** (either block builders running special-purpose code, or users that can relay transactions to block builders 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**. To prevent replay attacks (both cross-chain and multiple `EntryPoint` implementations), the `signature` should depend on `chainid` and the `EntryPoint` address. The core interface of the entry point contract is as follows: -```c++ +```solidity function handleOps(UserOperation[] calldata ops, address payable beneficiary); function handleAggregatedOps( @@ -74,104 +76,117 @@ function handleAggregatedOps( address payable beneficiary ); -function simulateValidation - (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; } +function simulateValidation(UserOperation calldata userOp); + +error ValidationResult(uint256 preOpGas, uint256 prefund, uint256 deadline, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); + +error ValidationResultWithAggregation(uint256 preOpGas, uint256 prefund, uint256 deadline, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, AggregatorStakeInfo aggregatorInfo); + +struct StakeInfo { + uint256 stake; + uint256 unstakeDelaySec; +} + +struct AggregatorStakeInfo { + address actualAggregator; + StakeInfo stakeInfo; +} ``` -The core interface required for a wallet to have is: +The core interface required for an account to have is: ```solidity -interface IWallet { +interface IAccount { function validateUserOp - (UserOperation calldata userOp, bytes32 requestId, address aggregator, uint256 missingWalletFunds) - external; + (UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds) + external returns (uint256 deadline); } ``` -The wallet + +The account + * 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 +* The userOpHash is a hash over the userOp (except signature), entryPoint and chainId +* If the account does not support signature aggregation, it MUST validate the signature is a valid signature of the `userOpHash`, and + SHOULD return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error should revert. +* MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case current account's deposit is high enough) +* The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it) +* The `aggregator` SHOULD be ignored for accounts that don't use an aggregator +* The return value `deadline` is either zero (meaning "indefinitely"), or the last timestamp this request is deemed valid. + (or SIG_VALIDATION_FAILED on signature mismatch) + +An account that works with aggregated signature should have the interface: -A Wallet that works with aggregated signature should have the interface: ```solidity -interface IAggregatedWallet is IWallet { +interface IAggregatedAccount is IAccount { 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 + +* **getAggregator()** returns the aggregator this account supports. +* **validateUserOp()** (inherited from IAccount interface) MUST verify the `aggregator` parameter is valid and the same as `getAggregator` +* The account should also support aggregator-specific getter (e.g. `getAggregationInfo()`). + This method should export the account'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 validateUserOpSignature(UserOperation calldata userOp) + external view returns (bytes memory sigForUserOp); - function aggregateSignatures(bytes[] calldata sigsForAggregation) external view returns (bytes memory aggregatesSignature); + function aggregateSignatures(UserOperation[] calldata userOps) 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 +* If an account uses an aggregator (returns it with getAggregator()), then its address is returned by `simulateValidation()` reverting with `ValidationResultWithAggregator` instead of `ValidationResult` +* To accept the UserOp, the bundler must call **validateUserOpSignature()** to validate the userOp's signature. +* **aggregateSignatures()** must aggregate all UserOp signature into a single value. +* Note that the above methods are helper method for the bundler. The bundler MAY use a native library to perform the same validation and aggregation logic. * **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 +#### Using signature aggregators + +An account signify it uses signature aggregation by exposing the aggregator's address in the `getAggregator()` method. +During `simulateValidation`, this aggregator is returned (in the `ValidationResultWithAggregator`) -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. +The bundler should first accept the aggregator (validate its stake info and that it is not throttled/banned) +Then it MUST verify the userOp using `aggregator.validateUserOpSignature()` -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. +Signature aggregator SHOULD stake just like a paymaster, unless it is exempt due to not accessing global storage - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. 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, or if the signature aggregation fails. ### 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. + +* `handleOps` handle userOps of accounts 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. +* `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-account 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 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) +* **Create the account if it does not yet exist**, using the initcode provided in the `UserOperation`. If the account 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 account**, passing in the `UserOperation`, the required fee and aggregator (if there is one). The account should verify the operation's signature, and pay the fee if the account considers the operation valid. If any `validateUserOp` call fails, `handleOps` must skip execution of at least that operation, and may revert entirely. +* Validate the account'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 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. +* **Call the account with the `UserOperation`'s calldata**. It's up to the account to choose how to parse the calldata; an expected workflow is for the account to have an `execute` function that parses the remaining calldata as a series of one or more calls that the account should make. ![](../assets/eip-4337/image1.png) @@ -180,22 +195,22 @@ A node/bundler SHOULD drop (and not add to the mempool) `UserOperation` that fai ### 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 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: +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](./eip-20.md) 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. 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 verification loop, in addition to calling `validateUserOp`, the `handleOps` execution also must check that the paymaster 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 `missingAccountFunds` of 0 to reflect that the account'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. +If the paymaster's validatePaymasterUserOp returns a "context", then `handleOps` 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. -Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a paymaster reputation system; see the [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-paymasters) for details. +Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a reputation system. paymaster must either limit its storage usage, or have a stake. see the [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. The paymaster interface is as follows: ```c++ function validatePaymasterUserOp - (UserOperation calldata userOp, bytes32 requestId, uint256 maxCost) - external returns (bytes memory context); + (UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) + external returns (bytes memory context, uint256 deadline); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost) @@ -208,7 +223,6 @@ enum PostOpMode { } ``` -To prevent attacks involving malicious `UserOperation` objects listing other users' wallets as their paymasters, the entry point contract must require a paymaster to call the entry point to lock their stake and thereby consent to being a paymaster. Unlocking stake must have a delay. The extended interface for the entry point, adding functions for paymasters to add and withdraw stake, is: ```c++ // add a paymaster stake (must be called by the paymaster) @@ -221,7 +235,10 @@ function unlockStake() external function withdrawStake(address payable withdrawAddress) external ``` -The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. The entry point must implement the following interface to allow paymasters (and optionally wallets) manage their deposit: +The paymaster must also have a deposit, which the entry point will charge UserOperation costs from. +The deposit (for paying gas fees) is separate from the stake (which is locked). + +The entry point must implement the following interface to allow paymasters (and optionally accounts) manage their deposit: ```c++ // return the deposit of an account @@ -232,70 +249,109 @@ function depositTo(address account) public payable // withdraw from the deposit function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external - ``` ### Client behavior upon receiving a UserOperation 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 `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`. -- The sender doesn't have another `UserOperation` already present in the pool (or it replaces an existing entry with the same sender and nonce, with a higher `maxPriorityFeePerGas` and an equally increased `maxFeePerGas`). Only one `UserOperation` per sender may be included in a single batch. +* Either the `sender` is an existing contract, or the `initCode` is not empty (but not both) +* If `initCode` is not empty, parse its first 20 bytes as a factory address. Record whether the factory is staked, in case the later simulation indicates that it needs to be. If the factory accesses global state, it must be staked - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. +* 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 `paymasterAndData` is either empty, or start with the **paymaster** address, which is a contract that (i) currently has nonempty code on chain, (ii) has a sufficient deposit to pay for the UserOperation, and (iii) is not currently banned. During simulation, the paymaster's stake is also checked, depending on its storage usage - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. +* 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`. +* The sender doesn't have another `UserOperation` already present in the pool (or it replaces an existing entry with the same sender and nonce, with a higher `maxPriorityFeePerGas` and an equally increased `maxFeePerGas`). Only one `UserOperation` per sender may be included in a single batch. A sender is exempt from this rule and may have multiple `UserOperations` in the pool and in a batch if it is staked (see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) below), but this exception is of limited use to normal accounts. -If the `UserOperation` object passes these sanity checks, the client must next run the first op simulation, and if the simulation succeeds, the client must add the op to the pool. A second simulation must also happen during bundling to make sure that the storage accessed is the same as the `accessList` that was saved during the initial simulation. +If the `UserOperation` object passes these sanity checks, the client must next run the first op simulation, and if the simulation succeeds, the client must add the op to the pool. A second simulation must also happen during bundling to make sure the UserOperation is still valid. ### Simulation -To simulate a `UserOperation` validation, the client makes a view call to `simulateValidation(userop)`, with a "from" address set to all-zeros +#### Simulation Rationale + +In order to add a UserOperation into the mempool (and later to add it into a bundle) we need to "simulate" it to make sure it is valid, and that it is capable of paying for its own execution. +In addition, we need to verify that the same will hold true when executed on-chain. +For this purpose, a UserOperation is not allowed to access any information that might change between simulation and execution, such as current block time, number, hash etc. +In addition, a UserOperation is only allowed to access data related to this sender address: Multiple UserOperations should not access the same storage, so that it is impossible to invalidate a large number of UserOperations with a single state change. +There are 3 special contracts that interact with the account: the factory (initCode) that deploys the contract, the paymaster that can pay for the gas, and signature aggregator (described later) +Each of these contracts is also restricted in its storage access, to make sure UserOperation validations are isolated. + +#### Specification: + +To simulate a `UserOperation` validation, the client makes a view call to `simulateValidation(userop)` -If the call returns an error, the client rejects this `userOp`. +This method always revert with `ValidationResult` as successful response. +If the call reverts with other error, the client rejects this `userOp`. -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, by calling: + +1. If `initCode` is present, create the account. +2. `account.validateUserOp`. +3. if specified a paymaster: `paymaster.validatePaymasterUserOp`. + +Either `validateUserOp` or `validatePaymasterUserOp` may return a "deadline", which is the latest timestamp that this UserOperation is valid on-chain. +the simulateValidation call returns the minimum of those deadlines. +A node MAY drop a UserOperation if the deadline is too soon (e.g. wouldn't make it to the next block) 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. +In order to distinguish between them, there is a call to the NUMBER opcode (`block.number`), used as a delimiter between the 3 functions. While simulating `userOp` validation, the client should make sure that: -1. Neither call's execution trace invokes any **forbidden opcodes** -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: - -```c++ -function getSenderStorage(address sender) external view returns (uint256[] memory senderStorageCells) -``` +1. May not invokes any **forbidden opcodes** +2. Must not use GAS opcode (unless followed immediately by one of { `CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL` }.) +3. Storage access is limited as follows: + 1. self storage (of factory/paymaster, respectively) is allowed, but only if self entity is staked + 2. account storage access is allowed (see Storage access by Slots, below), + 3. in any case, may not use storage used by another UserOp `sender` in the same bundle (that is, paymaster and factory are not allowed as senders) +4. Limitation on "CALL" opcodes (`CALL`, `DELEGATECALL`, `CALLCODE`, `STATICCALL`): + 1. must not use value (except from account to the entrypoint) + 2. must not revert with out-of-gas + 3. destination address must have code (EXTCODESIZE>0) + 4. cannot call EntryPoint's `handleOps` method (to avoid recursion) +5. `EXTCODEHASH` of every address accessed (by any opcode) does not change between first and second simulations of the op. +6. `EXTCODEHASH`, `EXTCODELENGTH`, `EXTCODECOPY` may not access address with no code. +7. If `op.initcode.length != 0` , allow only one `CREATE2` opcode call (in the first (deployment) block), otherwise forbid `CREATE2`. + +#### Storage associated with an address + +We define storage slots as "associated with an address" as all the slots that uniquely related on this address, and cannot be related with any other address. +In solidity, this includes all storage of the contract itself, and any storage of other contracts that use this contract address as a mapping key. + +An address `A` is associated with: + +1. Slots of contract `A` address itself. +2. Slot `A` on any other address. +3. Slots of type `keccak256(A || X) + n` on any other address. (to cover `mapping(address => value)`, which is usually used for balance in EIP-20 tokens). + `n` is an offset value up to 128, to allow accessing fields in the format `mapping(address => struct)` + + +#### Alternative Mempools + +The simulation rules above are strict and prevent the ability of paymasters and signature aggregators to grief the system. +However, there might be use-cases where specific paymasters (and signature aggregators) can be validated +(through manual auditing) and verified that they cannot cause any problem, while still require relaxing of the opcode rules. +A bundler cannot simply "whitelist" request from a specific paymaster: if that paymaster is not accepted by all +bundlers, then its support will be sporadic at best. +Instead, we introduce the term "alternate mempool". +UserOperations that use whitelisted paymasters (or signature aggregators) are put into a separate mempool. +Only bundlers that support this whitelist will use UserOperations from this mempool. +These UserOperations can be bundled together with UserOperations from the main mempool ### Bundling 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 + +* Exclude UserOps that access any sender address created by another UserOp on the same batch (via a 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. -- If the call reverted, check the `FailedOp` event. A `FailedOp` during `handleOps` simulation is an unexpected event since it was supposed to be caught by the single-UserOperation simulation. Remove the failed op that caused the revert from the batch and drop from the mempool. Other ops from the same paymaster should be removed from the current batch, but kept in the mempool. Repeat until `eth_estimateGas` succeeds. -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). +* Run `eth_estimateGas` with maximum possible gas, to verify the entire `handleOps` batch transaction, and use the estimated gas for the actual transaction execution. +* If the call reverted, check the `FailedOp` event. A `FailedOp` during `handleOps` simulation is an unexpected event since it was supposed to be caught by the single-UserOperation simulation. Remove the failed op that caused the revert from the batch and drop from the mempool. Other ops from the same paymaster should be removed from the current batch, but kept in the mempool. Repeat until `eth_estimateGas` succeeds. + +In practice, restrictions (2) and (3) basically mean that the only external accesses that the account 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` @@ -303,27 +359,50 @@ When a bundler includes a bundle in a block it must ensure that earlier transact #### Forbidden opcodes -The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the wallet, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain. +The forbidden opcodes are to be forbidden when `depth > 2` (i.e. when it is the factory, account, paymaster, or other contracts called by them that are being executed). They are: `GASPRICE`, `GASLIMIT`, `DIFFICULTY`, `TIMESTAMP`, `BASEFEE`, `BLOCKHASH`, `NUMBER`, `SELFBALANCE`, `BALANCE`, `ORIGIN`, `GAS`, `CREATE`, `COINBASE`, `SELFDESTRUCT`. They should only be forbidden during verification, not execution. These opcodes are forbidden because their outputs may differ between simulation and execution, so simulation of calls using these opcodes does not reliably tell what would happen if these calls are later done on-chain. 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 +### Reputation scoring and throttling/banning for global entities + +#### ReputationRationale. + +UserOperation's storage access rules prevent them from interfere with each other. +But "global" entities - paymasters, factories and aggregators are accessed by multiple UserOperations, and thus might invalidate multiple previously-valid UserOperations. + +To prevent abuse, we throttle down (or completely ban for a period of time) an entity that causes invalidation of large number of UserOperations in the mempool. +To prevent such entities from "sybil-attack", we require them to stake with the system, and thus make such DoS attack very expensive. +Note that this stake is never slashed, and can be withdrawn any time (after unstake delay) +The only exemption from staking is if the entity doesn't use storage during validation. +(unstaked entity may use storage [associated with the sender](#storage-associated-with-an-address)) +When staked, an entity is also allowed to use its own associated storage, in addition to sender's associated storage. + +The stake value is not enforced on-chain, but specifically by each node while simulating a transaction. +The stake is expected to be above MIN_STAKE_VALUE, and unstake delay above MIN_UNSTAKE_DELAY +The value of MIN_UNSTAKE_DELAY is 84600 (one day) +The value of MIN_STAKE_VALUE is determined per chain, and specified in the "bundler specification test suite" -Clients maintain two mappings with a value for each paymaster: +#### Specification. + +In the following specification, "entity" is either address that is explicitly referenced by the UserOperation: sender, factory, paymaster and aggregator. +Clients maintain two mappings with a value for staked entities: * `opsSeen: Map[Address, int]` * `opsIncluded: Map[Address, int]` -When the client learns of a new `paymaster`, it sets `opsSeen[paymaster] = 0` and `opsIncluded[paymaster] = 0` . +If an entity doesn't use storage at all, or only reference storage associated with the "sender" (see [Storage associated with an address](#storage-associated-with-an-address)), then it is considered "OK", without using the rules below. + +When the client learns of a new staked entity, it sets `opsSeen[paymaster] = 0` and `opsIncluded[paymaster] = 0` . -The client sets `opsSeen[paymaster] +=1` each time it adds an op with that `paymaster` to the `UserOperationPool`, and the client sets `opsIncluded[paymaster] += 1` each time an op that was in the `UserOperationPool` is included on-chain. +The client sets `opsSeen[entity] +=1` each time it adds an op with that `entity` to the `UserOperationPool`, and the client sets `opsIncluded[entity] += 1` each time an op that was in the `UserOperationPool` is included on-chain. -Every hour, the client sets `opsSeen[paymaster] -= opsSeen[paymaster] // 24` and `opsIncluded[paymaster] -= opsIncluded[paymaster] // 24` for all paymasters (so both values are 24-hour exponential moving averages). +Every hour, the client sets `opsSeen[entity] -= opsSeen[entity] // 24` and `opsIncluded[entity] -= opsIncluded[entity] // 24` for all entities (so both values are 24-hour exponential moving averages). -We define the **status** of a paymaster as follows: +We define the **status** of an entity as follows: ```python OK, THROTTLED, BANNED = 0, 1, 2 @@ -342,7 +421,7 @@ def status(paymaster: Address, return BANNED ``` -Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` of all ops seen on the network to get included. If a paymaster falls too far behind this minimum, the paymaster gets **throttled** (meaning, the client does not accept ops from that paymaster if there is already an op from that paymaster, and an op only stays in the pool for 10 blocks), If the paymaster falls even further behind, it gets **banned**. Throttling and banning naturally reverse over time because of the exponential-moving-average rule. +Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` of all ops seen on the network to get included. If an entity falls too far behind this minimum, it gets **throttled** (meaning, the client does not accept ops from that paymaster if there is already an op with that entity, and an op only stays in the pool for 10 blocks), If the entity falls even further behind, it gets **banned**. Throttling and banning naturally decay over time because of the exponential-moving-average rule. **Non-bundling clients and bundlers should use different settings for the above params**: @@ -354,16 +433,87 @@ Stated in simpler terms, we expect at least `1 / MIN_INCLUSION_RATE_DENOMINATOR` To help make sense of these params, note that a malicious paymaster can at most cause the network (only the p2p network, not the blockchain) to process `BAN_SLACK * MIN_INCLUSION_RATE_DENOMINATOR / 24` non-paying ops per hour. -### RPC methods +## Rationale + +The main challenge with a purely smart contract wallet based account abstraction system is DoS safety: how can a block builder including an operation make sure that it will actually pay fees, without having to first execute the entire operation? Requiring the block builder 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 accounts 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 account itself, cannot use environment opcodes (eg. `TIMESTAMP`), and can only edit the storage of the account, 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 block builders 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 accounts' logic simple. The alternative would be to require accounts 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. + +### Paymasters + +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 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 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 account 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. + +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. +If the factory does use CREATE2 or some other deterministic method to create the wallet, it's expected to return the wallet address even if the wallet has already been created. This is to make it easier for clients to query the address without knowing if the wallet has already been deployed, by simulating a call to `entryPoint.getSenderAddress()`, which calls the factory under the hood. +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. +The factory has to be staked if it accesses global storage - see [reputation, throttling and banning section](#reputation-scoring-and-throttlingbanning-for-global-entities) for details. + +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.getSenderAddress()` + +### Entry point upgrading + +Accounts are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow account upgradability. The account code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their account's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel. -`eth_sendUserOperation` +### RPC methods (eth namespace) -eth_sendUserOperation submits a User Operation object to the User Operation pool of the client. An entryPoint address `MUST` be specified, and the client `MUST` only simulate and submit the User Operation through the specified entryPoint. +#### * eth_sendUserOperation -The result `SHOULD` be set to true if and only if the request passed simulation and was accepted in the client's User Operation pool. If the validation, simulation, or User Operation pool inclusion fails, `result` `SHOULD NOT` be returned. Rather, the client `SHOULD` return the failure reason. +eth_sendUserOperation submits a User Operation object to the User Operation pool of the client. The client MUST validate the UserOperation, and return a result accordingly. + +The result `SHOULD` be set to the **userOpHash** if and only if the request passed simulation and was accepted in the client's User Operation pool. If the validation, simulation, or User Operation pool inclusion fails, `result` `SHOULD NOT` be returned. Rather, the client `SHOULD` return the failure reason. + +##### Parameters: + +1. **UserOperation** a full user-operation struct. All fields MUST be set as hex values. empty `bytes` block (e.g. empty `initCode`) MUST be set to `"0x"` +2. **EntryPoint** the entrypoint address the request should be sent through. this MUST be one of the entry points returned by the `supportedEntryPoints` rpc call. + +##### Return value: + +* If the UserOperation is valid, the client MUST return the calculated **userOpHash** for it +* in case of failure, MUST return an `error` result object, with `code` and `message`. The error code and message SHOULD be set as follows: + * **code: -32602** - invalid UserOperation struct/fields + * **code: -32500** - transaction rejected by entryPoint's simulateValidation, during wallet creation or validation + * The `message` field MUST be set to the FailedOp's "`AAxx`" error message from the EntryPoint + * **code: -32501** - transaction rejected by paymaster's validatePaymasterUserOp + * The `message` field SHOULD be set to the revert message from the paymaster + * The `data` field MUST contain a `paymaster` value + * **code: -32502** - transaction rejected because of opcode validation + * **code: -32503** - UserOperation expires shortly: either wallet or paymaster returned an `deadline` that will expire soon + * The `data` field SHOULD contain a `deadline` value + * The `data` field SHOULD contain a `paymaster` value, if this error was triggered by the paymaster + * **code: -32504** - transaction rejected because paymaster (or signature aggregator) is throttled/banned + * The `data` field SHOULD contain a `paymaster` or `aggregator` value, depending on the failed entity + * **code: -32505** - transaction rejected because paymaster (or signature aggregator) stake or unstake-delay is too low + * The `data` field SHOULD contain a `paymaster` or `aggregator` value, depending on the failed entity + * The `data` field SHOULD contain a `minimumStake` and `minimumUnstakeDelay` + * **code: -32506** - transaction rejected because wallet specified unsupported signature aggregator + * The `data` field SHOULD contain an `aggregator` value + +##### Example: + +Request: ```json= -# Request { "jsonrpc": "2.0", "id": 1, @@ -386,17 +536,110 @@ The result `SHOULD` be set to true if and only if the request passed simulation ] } -# Response +``` + +Response: + +``` { "jsonrpc": "2.0", "id": 1, - "result": true + "result": "0x1234...5678" } ``` -`eth_supportedEntryPoints` +##### Example failure responses: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "message": "AA21 didn't pay prefund", + "code": -32500 + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "error": { + "message": "paymaster stake too low", + "data": { + "paymaster": "0x123456789012345678901234567890123456790", + "minimumStake": "0xde0b6b3a7640000", + "minimumUnstakeDelay": "0x15180" + }, + "code": -32504 + } +} +``` + + +#### * eth_estimateUserOperationGas + +Estimate the gas values for a UserOperation. +Given UserOperation optionally without gas limits and gas prices, return the needed gas limits. +The signature field is ignored by the wallet, so that the operation will not require user's approval. +Still, it might require putting a "semi-valid" signature (e.g. a signature in the right length) + +**Parameters**: same as `eth_sendUserOperation` + gas limits (and prices) parameters are optional, but are used if specified. + `maxFeePerGas` and `maxPriorityFeePerGas` default to zero, so no payment is required by neither account nor paymaster. + +**Return Values:** + +* **preVerificationGas** gas overhead of this UserOperation +* **verificationGasLimit** actual gas used by the validation of this UserOperation +* **callGasLimit** value used by inner account execution + +##### Error Codes: -eth_supportedEntryPoints returns an array of the entryPoint addresses supported by the client. The first element of the array `SHOULD` be the entryPoint addressed preferred by the client. +Same as `eth_sendUserOperation` +This operation may also return an error if the inner call to the account contract reverts. + +#### * eth_getUserOperationByHash + +Return a UserOperation based on a hash (userOpHash) returned by `eth_sendUserOperation` + +**Parameters** + +* **hash** a userOpHash value returned by `eth_sendUserOperation` + +**Return value**: + +`null` in case the UserOperation is not yet included in a block, or a full UserOperation, with the addition of `entryPoint`, `blockNumber`, `blockHash` and `transactionHash` + +#### * eth_getUserOperationReceipt + +Return a UserOperation receipt based on a hash (userOpHash) returned by `eth_sendUserOperation` + +**Parameters** + +* **hash** a userOpHash value returned by `eth_sendUserOperation` + +**Return value**: + +`null` in case the UserOperation is not yet included in a block, or: + +* **userOpHash** the request hash +* **entryPoint** +* **sender** +* **nonce** +* **paymaster** the paymaster used for this userOp (or empty) +* **actualGasCost** - actual amount paid (by account or paymaster) for this UserOperation +* **actualGasUsed** - total gas used by this UserOperation (including preVerification, creation, validation and execution) +* **success** boolean - did this execution completed without revert +* **reason** in case of revert, this is the revert reason +* **logs** the logs generated by this UserOperation (not including logs of other UserOperations in the same bundle) +* **receipt** the TransactionReceipt object. + Note that the returned TransactionReceipt is for the entire bundle, not only for this UserOperation. + +#### * eth_supportedEntryPoints + +Returns an array of the entryPoint addresses supported by the client. The first element of the array `SHOULD` be the entryPoint addressed preferred by the client. ```json= # Request @@ -418,48 +661,232 @@ eth_supportedEntryPoints returns an array of the entryPoint addresses supported } ``` -## Rationale +#### * eth_chainId -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. +Returns [EIP-155](./eip-155.md) Chain ID. -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. +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "eth_chainId", + "params": [] +} -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. +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" +} +``` -### Paymasters +### RPC methods (debug Namespace) -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: +This api must only be available on testing mode and is required by the compatibility test suite. In production, any `debug_*` rpc calls should be blocked. -* 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 +#### * debug_bundler_clearState -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). +Clears the bundler mempool and reputation data of paymasters/accounts/factories/aggregators. -### First-time wallet creation +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_clearState", + "params": [] +} -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. +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` -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. +#### * debug_bundler_dumpMempool -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()` +Dumps the current UserOperations mempool -### Entry point upgrading +**Parameters:** + +* **EntryPoint** the entrypoint used by eth_sendUserOperation + +**Returns:** + +`array` - Array of UserOperations currently in the mempool. + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_dumpMempool", + "params": ["0x1306b01bC3e4AD202612D3843387e94737673F53"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": [ + { + sender, // address + nonce, // uint256 + initCode, // bytes + callData, // bytes + callGasLimit, // uint256 + verificationGasLimit, // uint256 + preVerificationGas, // uint256 + maxFeePerGas, // uint256 + maxPriorityFeePerGas, // uint256 + paymasterAndData, // bytes + signature // bytes + } + ] +} +``` + +#### * debug_bundler_sendBundleNow + +Forces the bundler to build and execute a bundle from the mempool as `handleOps()` transaction. + +Returns: `transactionHash` + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_sendBundleNow", + "params": [] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0xdead9e43632ac70c46b4003434058b18db0ad809617bd29f3448d46ca9085576" +} +``` + +#### * debug_bundler_setBundlingMode + +Sets bundling mode. -Wallets are encouraged to be DELEGATECALL forwarding contracts for gas efficiency and to allow wallet upgradability. The wallet code is expected to hard-code the entry point into their code for gas efficiency. If a new entry point is introduced, whether to add new functionality, improve gas efficiency, or fix a critical security bug, users can self-call to replace their wallet's code address with a new code address containing code that points to a new entry point. During an upgrade process, it's expected that two mempools will run in parallel. +After setting mode to "manual", an explicit call to debug_bundler_sendBundleNow is required to send a bundle. + +##### parameters: + +`mode` - 'manual' | 'auto' + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_setBundlingMode", + "params": ["manual"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` + +#### * debug_bundler_setReputation + +Sets reputation of given addresses. parameters: + +**Parameters:** + +* An array of reputation entries to add/replace, with the fields: + + * `address` - The address to set the reputation for. + * `opsSeen` - number of times a user operations with that entity was seen and added to the mempool + * `opsIncluded` - number of times a user operations that uses this entity was included on-chain + * `status` - (string) The status of the address in the bundler 'ok' | 'throttled' | 'banned'. + +* **EntryPoint** the entrypoint used by eth_sendUserOperation + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_setReputation", + "params": [ + [ + { + "address": "0x7A0A0d159218E6a2f407B99173A2b12A6DDfC2a6", + "opsSeen": 20, + "opsIncluded": 13 + } + ], + "0x1306b01bC3e4AD202612D3843387e94737673F53" + ] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": "ok" +} +``` + + +#### * debug_bundler_dumpReputation + +Returns the reputation data of all observed addresses. +Returns an array of reputation objects, each with the fields described above in `debug_bundler_setReputation` with the + + +**Parameters:** + +* **EntryPoint** the entrypoint used by eth_sendUserOperation + +**Return value:** + +An array of reputation entries with the fields: + +* `address` - The address to set the reputation for. +* `opsSeen` - number of times a user operations with that entity was seen and added to the mempool +* `opsIncluded` - number of times a user operations that uses this entity was included on-chain +* `status` - (string) The status of the address in the bundler 'ok' | 'throttled' | 'banned'. + +```json= +# Request +{ + "jsonrpc": "2.0", + "id": 1, + "method": "debug_bundler_dumpReputation", + "params": ["0x1306b01bC3e4AD202612D3843387e94737673F53"] +} + +# Response +{ + "jsonrpc": "2.0", + "id": 1, + "result": [ + { "address": "0x7A0A0d159218E6a2f407B99173A2b12A6DDfC2a6", + "opsSeen": 20, + "opsIncluded": 19, + "status": "ok" + } + ] +} +``` ## Backwards Compatibility -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. +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-[EIP-4337](./eip-4337.md) accounts, because those accounts do not have a `validateUserOp` function. If the account has a function for authorizing a trusted op submitter, then this could be fixed by creating an [EIP-4337](./eip-4337.md) compatible account that re-implements the verification logic as a wrapper and setting it to be the original account's trusted op submitter. ## Reference Implementation @@ -467,13 +894,13 @@ See `https://github.com/eth-infinitism/account-abstraction/tree/main/contracts` ## 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_ [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. +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). In total, this architecture reduces auditing and formal verification load for the ecosystem, because the amount of work that individual _accounts_ 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): -* **Safety against arbitrary hijacking**: The entry point only calls a wallet generically if `validateUserOp` to that specific wallet has passed (and with `op.calldata` equal to the generic call's calldata) +* **Safety against arbitrary hijacking**: The entry point only calls an account generically if `validateUserOp` to that specific account has passed (and with `op.calldata` equal to the generic call's calldata) * **Safety against fee draining**: If the entry point calls `validateUserOp` and passes, it also must make the generic call with calldata equal to `op.calldata` ## Copyright -Copyright and related rights waived via [CC0](../LICENSE.md). +Copyright and related rights waived via [CC0](../LICENSE.md). From 452992c400fddd5747c690d78774505965d11bc7 Mon Sep 17 00:00:00 2001 From: Firn Protocol <93839494+firnprotocol@users.noreply.github.com> Date: Thu, 29 Dec 2022 11:29:46 -0500 Subject: [PATCH 094/274] add material to EIP-5630 (#6235) * first commit for update to EIP-5630 * add material on encrypting to contract * linting fixes. add blank lines and links. --- EIPS/eip-5630.md | 119 ++++++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 49 deletions(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index 445bd02506116a..1d96108a1693cb 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -13,38 +13,25 @@ created: 2022-09-07 ## Abstract -This EIP proposes a new way to encrypt and decrypt using Ethereum keys. This EIP uses separate, unlinkable, pseudorandom keys for signing and encryption; it uses _only_ the `secp256k1` curve, and it uses a standardized version of ECIES. In contrast, other EIPs reused secret keys across both signing and encryption, and moreover reused the same secret key across both the `secp256k1` and `ec25519` curves. +This EIP proposes a new way to encrypt and decrypt using Ethereum keys. This EIP uses _only_ the `secp256k1` curve, and it uses a standardized version of ECIES. In contrast, a previous EIPs used the same secret key, in both signing and encryption, on two _different_ curves (namely, `secp256k1` and `ec25519`). ## Motivation -We discuss a few motivating examples. In a certain common design pattern, a dApp generates a fresh secret on behalf of a user. It is of interest if, instead of forcing this user to independently store, safeguard, and back up this latter secret, the dApp may instead encrypt this secret to a public key which the user controls—and whose secret key, crucially, can be derived deterministically from the user's HD wallet hierarchy—and then post the resulting ciphertext to secure storage (e.g., on-chain). +We discuss a few motivating examples. One key motivation is direct-to-address encryption on Ethereum. Using our EIP, one can directly send encrypted messages to some desired recipient on-chain, without having a prior direct channel to that recipient. (Note that in this EIP, we standardize _only_ the encryption procedure—that is, the generation of the ciphertext—and _not_ how exactly the on-chain message should be sent. In practice, ideally, smart-contract infrastructure will be set up for this purpose; barring this, encryptors could make use of the raw `data` field available in each standard transfer.) -This design pattern allows the dApp/user to bootstrap the security of the _fresh_ secret onto the security of the user's existing HD wallet seed phrase, which the user has already gone through the trouble of safeguarding and storing. This represents a far lower UX burden than forcing the user to store and manage fresh keys directly (which can, and often does, lead to loss of funds). We note that this _exact_ design pattern described above is used today by, e.g., Tornado Cash. - -As a separate motivation, we mention the possibility of dApps which facilitate end-to-end encrypted messaging. +We discuss a second sort of example. In a certain common design pattern, a dApp generates a fresh secret on behalf of a user. It is of interest if, instead of forcing this user to independently store, safeguard, and back up this latter secret, the dApp may instead encrypt this secret to a public key which the user controls—and whose secret key, crucially, resides within the user's HD wallet hierarchy—and then post the resulting ciphertext to secure storage (e.g., on-chain). This design pattern allows the dApp/user to bootstrap the security of the _fresh_ secret onto the security of the user's existing HD wallet seed phrase, which the user has already gone through the trouble of safeguarding and storing. This represents a far lower UX burden than forcing the user to store and manage fresh keys directly (which can, and often does, lead to loss of funds). We note that this design pattern described above is used today by, various dApps (e.g., Tornado Cash). ## Specification -We describe our approach here; we compare our approach to other EIPs in the **Rationale** section below. +We describe our approach here; we compare our approach to prior EIPs in the **Rationale** section below. -We use the `secp256k1` curve for both signing and encryption (with different keys, see below). -In the latter case, we use ECIES; specifically, we use a standardized variant. -Specifically, we propose the choices: +We use the `secp256k1` curve for both signing and encryption. +For encryption, we use ECIES. Specifically, we propose the standardized choices: - the KDF `ANSI-X9.63-KDF`, where the hash function `SHA-512` is used, - the HMAC `HMAC–SHA-256–256 with 32 octet or 256 bit keys`, - the symmetric encryption scheme `AES–256 in CBC mode`. -We finally describe a method to derive encryption secret keys deterministically—but pseudorandomly—from signing keys, in such a way that a natural one-to-one relationship obtains between these keys (this latter property is essential, since it allows Ethereum accounts to be used as handles onto encryption/decryption keys, as both the former and current API interfaces do). -Indeed, we propose that, given a signing private key _sk_ ∈ 𝔽_q—which is naturally represented as a 32-byte big-endian byte string—the corresponding decryption key _dk_ ∈ 𝔽_q be generated as the 32-byte secret: - -```solidity - dk := ANSI-X9.63-KDF(sk), -``` - -where moreover the _Ethereum `keccak256`_ hash is used for this KDF. This latter decision is essentially for implementation convenience; indeed, MetaMask's `eth-simple-keyring` already has something close to this functionality built in, and it requires only a minimal code change (see our implementation below). -We set _SharedInfo_ to be empty here. - We propose that the binary, _concatenated_ serialization mode for ECIES ciphertexts be used, both for encryption and decryption, where moreover elliptic curve points are _compressed_. This approach is considerably more space-efficient than the prior approach, which outputted a stringified JSON object (itself containing base64-encoded fields). We moreover propose that binary data be serialized to and from `0x`-prefixed hex strings. We moreover use `0x`-prefixed hex strings to specify private keys and public keys, and represent public keys in compressed form. We represent Ethereum accounts in the usual way (`0x`-prefixed, 20-byte hex strings). @@ -58,10 +45,10 @@ request({ ``` where `account` is a standard 20-byte, `0x`-prefixed, hex-encoded Ethereum account, the client should operate as follows: - - find the secret signing key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. - - compute the 32-byte secret `dk := ANSI-X9.63-KDF(sk)`, where the `keccak256` hash is used in the KDF. - - compute the `secp256k1` public key corresponding to `dk`. - - return this public key in compressed, `0x`-prefixed, hex-encoded form. + +- find the secret signing key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. +- compute the `secp256k1` public key corresponding to `sk`. +- return this public key in compressed, `0x`-prefixed, hex-encoded form. On the request @@ -73,45 +60,77 @@ request({ ``` where `account` is as above, and `encryptedMessage` is a JSON object with the properties `version` (an arbitrary string) and `ciphertext` (a `0x`-prefixed, hex-encoded, bytes-like string), the client should operate as follows: - - perform a `switch` on the value `encryptedMessage.version`. if it equals: - - `x25519-xsalsa20-poly1305`, then use #1098's specification; - - `secp256k1-sha512kdf-aes256cbc-hmacsha256`, then proceed as described below; - - if it equals neither, throw an error. - - find the secret key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. - - compute the 32-byte secret `dk := ANSI-X9.63-KDF(sk)`, where the `keccak256` hash is used in the KDF. - - using `dk`, perform an ECIES decryption of `encryptedMessage.ciphertext`, where the above choices of parameters are used. - - decode the resulting binary plaintext as a `utf-8` string, and return it. + +- perform a `switch` on the value `encryptedMessage.version`. if it equals: + - `secp256k1-sha512kdf-aes256cbc-hmacsha256`, then break from the switch and proceed as in the bullets below; + - `x25519-xsalsa20-poly1305`, then, optionally, use #1098's specification _if_ backwards compatibility is desired, and otherwise fallthrough; + - `default`, throw an error. +- find the secret key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. +- using `sk`, perform an ECIES decryption of `encryptedMessage.ciphertext`, where the above choices of parameters are used. +- decode the resulting binary plaintext as a `utf-8` string, and return it. Test vectors are given below. + +### Encrypting to a smart contract + +In light of account abstraction, [EIP-4337](eip-4337.md), and the advent of smart-contract wallets, we moreover specify a way to encrypt to a contract. +More precisely, we specify a way for a contract to _advertise_ how it would like encryptions to it to be constructed. This should be viewed as an analogue of [EIP-1271](eip-1271.md), but for encryption, as opposed to signing. + +Our specification is as follows. + +```solidity +pragma solidity ^0.8.0; + +contract ERC5630 { + /** + * @dev Should return an encryption of the provided plaintext, using the provided randomness. + * @param plaintext_ Plaintext to be encrypted + * @param randomness_ Entropy to be used during encryption + function encryptTo(bytes memory plaintext, bytes32 randomness) + public + view + returns (string memory version, bytes memory ciphertext); +} +``` + +Each contract should implement `encryptToAccount` as it desires; for example, it could use our specification above (i.e., for some fixed public key depending on the contract), or something arbitrary. + ## Rationale There is _no security proof_ for a scheme which simultaneously invokes signing on the `secp256k1` curve and encryption on the `ec25519` curve, and where _the same secret key is moreover used in both cases_. Though no attacks are known, it is not desirable to use a scheme which lacks a proof in this way. -Certain papers have studied the reuse of the same key in signing and encryption, but where _the same curve is used in both_ (e.g., in the context of EMV payments). Those papers have found the joint scheme to be secure in the generic group model. -Though this result provides _some level of_ assurance of security of this joint scheme (where, we stress, _only one_ curve is used), it is at least as secure to use different, pseudorandomly unlinkable keys for signing and encryption. Indeed, we note that if the hash function is modeled as a random oracle, then each decryption key `dk` is completely random, and in particular uncorrelated with its corresponding signing key. +We, instead, propose the reuse of the same key in signing and encryption, but where _the same curve is used in both_. This very setting has been studied in prior work; see, e.g., Degabriele, Lehmann, Paterson, Smart and Strefler, _On the Joint Security of Encryption and Signature in EMV_, 2011. That work found this joint scheme to be secure in the generic group model. +We note that this very joint scheme (i.e., using ECDSA and ECIES on the same curve) is used live in production in EMV payments. + +We now discuss a few further aspects of our approach. + +**On-chain public key discovery.** Our proposal has an important feature whereby an encryption _to_ some account can be constructed whenever that account has signed at least one transaction. +Indeed, it is possible to recover an account's `secp256k1` public key directly from any signature on behalf of that account. + +**Twist attacks.** A certain GitHub post by Christian Lundkvist warns against "twist attacks" on the `secp256k1` curve. These attacks are not applicable to this EIP, for multiple _distinct_ reasons, which we itemize: +- **Only applies to ECDH, not ECIES.** This attack only applies to a scenario in which an attacker can induce a victim to exponentiate an attacker-supplied point by a sensitive scalar, and then moreover send the result back to the attacker. But this pattern only happens in ECDH, and never ECIES. Indeed, in ECIES, we recall that the only sensitive Diffie–Hellman operation happens during decryption, but in this case, the victim (who would be the decryptor) never sends the resulting DH point back to the attacker (rather, the victim merely uses it locally to attempt an AES decryption). During _encryption_, the exponentiation is done by the encryptor, who has no secret at all (sure enough, the exponentiation is by an ephemeral scalar), so here there would be nothing for the attacker to learn. +- **Only applies to uncompressed points.** Indeed, we use compressed points in this EIP; when compressed points are used, any 32-byte string supplied by an attacker will resolve canonically to a point on the right curve, or else generate an error; there is no possibility of a "wrong curve" point. +- **Only applies when you fail to check a point is on the curve.** But this is inapplicable for us anyway, since we use compressed points (see above). ## Backwards Compatibility -The previous proposal stipulated that encryption and decryption requests contain a `version` string. Our proposal merely adds a case for this string; encryption and decryption requests under the existing scheme will be handled identically. Unfortunately, the previous proposal did _not_ include a version string in `encryptionPublicKey`, and merely returned the `ec25519` public key directly as a string. We thus propose to immediately return the `secp256k1` public key, overwriting the previous behavior. The old behavior can be kept via a legacy method. -We note that the previous EIP is _not_ (to our knowledge) implemented in a non-deprecated manner in _any_ production code today, and the EIP stagnated. We thus have a lot of flexibility here; we only need enough backwards compatibility to allow dApps to migrate. +The previous EIP stipulated that encryption and decryption requests contain a `version` string. Our proposal merely adds a case for this string; encryption and decryption requests under the existing scheme will be handled identically. +The previous proposal did _not_ include a version string in `encryptionPublicKey`, and merely returned the `ec25519` public key directly as a string. We thus propose to immediately return the `secp256k1` public key, overwriting the previous behavior. +It is unlikely that this will be an issue, since encryption keys need be newly retrieved _only_ upon the time of encryption; on the other hand, _new_ ciphertexts will be generated using our new approach. + +In any case, the previous EIP was never standardized, and is _not_ (to our knowledge) implemented in a non-deprecated manner in _any_ production code today. We thus have a lot of flexibility here; we only need enough backwards compatibility to allow dApps to migrate. ### Test Cases -Starting from the secret _signing key_ +The secret _signing key_ ``` 0x439047a312c8502d7dd276540e89fe6639d39da1d8466f79be390579d7eaa3b2 ``` -with Ethereum address `0x72682F2A3c160947696ac3c9CC48d290aa89549c`, the `keccak256`-based KDF described above yields the secret _decryption key_ +with Ethereum address `0x72682F2A3c160947696ac3c9CC48d290aa89549c`, has `secp256k1` public key ``` - 0xecb4fbc91b48954259469d13d2e69c6fe4b57b73dd9dd277085b2d5e764a4023 -``` - -with `secp256k1` public key - -``` - 0x023e5feced05739d8aad239b037787ba763706fb603e3e92ff0a629e8b4ec2f9be + 0x03ff5763a2d3113229f2eda8305fae5cc1729e89037532a42df357437532770010 ``` Thus, the request: @@ -126,15 +145,15 @@ request({ should return: ```javascript -"0x023e5feced05739d8aad239b037787ba763706fb603e3e92ff0a629e8b4ec2f9be" +"0x03ff5763a2d3113229f2eda8305fae5cc1729e89037532a42df357437532770010" ``` -Encrypting the message `"My name is Satoshi Buterin"` under the above public key could yield, for example: +Encrypting the UTF-8 message `I use Firn Protocol to gain privacy on Ethereum.` under the above public key could yield, for example: ```javascript { version: 'secp256k1-sha512kdf-aes256cbc-hmacsha256', - ciphertext: '0x03ab54b1b866c5231787fddc2b4dfe9813b6222646b811a2a395040e24e098ae93e39ceedec5516dbf04dbd7b8f5f5030cde786f6aeed187b1d10965714f8d383c2240b4014809077248ddb66cc8bd86eb815dff0e42b0613bbdd3024532c19d0a', + ciphertext: '0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce', } ``` @@ -145,15 +164,17 @@ request({ method: 'eth_decrypt', params: [{ version: 'secp256k1-sha512kdf-aes256cbc-hmacsha256', - ciphertext: '0x03ab54b1b866c5231787fddc2b4dfe9813b6222646b811a2a395040e24e098ae93e39ceedec5516dbf04dbd7b8f5f5030cde786f6aeed187b1d10965714f8d383c2240b4014809077248ddb66cc8bd86eb815dff0e42b0613bbdd3024532c19d0a', + ciphertext: '0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce', }, "0x72682F2A3c160947696ac3c9CC48d290aa89549c"], }) ``` -should return the string `"My name is Satoshi Buterin"`. +should return the string `I use Firn Protocol to gain privacy on Ethereum.`. ## Security Considerations + Our proposal uses heavily standardized algorithms and follows all best practices. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 805feb30e3e36d3ac5dcc27e4eb0617afd9c4480 Mon Sep 17 00:00:00 2001 From: Firn Protocol <93839494+firnprotocol@users.noreply.github.com> Date: Thu, 29 Dec 2022 11:40:52 -0500 Subject: [PATCH 095/274] small cleanups (#6236) --- EIPS/eip-5630.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index 1d96108a1693cb..ad56f63de637b6 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -84,8 +84,9 @@ pragma solidity ^0.8.0; contract ERC5630 { /** * @dev Should return an encryption of the provided plaintext, using the provided randomness. - * @param plaintext_ Plaintext to be encrypted - * @param randomness_ Entropy to be used during encryption + * @param plaintext Plaintext to be encrypted + * @param randomness Entropy to be used during encryption + */ function encryptTo(bytes memory plaintext, bytes32 randomness) public view @@ -107,6 +108,7 @@ We now discuss a few further aspects of our approach. Indeed, it is possible to recover an account's `secp256k1` public key directly from any signature on behalf of that account. **Twist attacks.** A certain GitHub post by Christian Lundkvist warns against "twist attacks" on the `secp256k1` curve. These attacks are not applicable to this EIP, for multiple _distinct_ reasons, which we itemize: + - **Only applies to ECDH, not ECIES.** This attack only applies to a scenario in which an attacker can induce a victim to exponentiate an attacker-supplied point by a sensitive scalar, and then moreover send the result back to the attacker. But this pattern only happens in ECDH, and never ECIES. Indeed, in ECIES, we recall that the only sensitive Diffie–Hellman operation happens during decryption, but in this case, the victim (who would be the decryptor) never sends the resulting DH point back to the attacker (rather, the victim merely uses it locally to attempt an AES decryption). During _encryption_, the exponentiation is done by the encryptor, who has no secret at all (sure enough, the exponentiation is by an ephemeral scalar), so here there would be nothing for the attacker to learn. - **Only applies to uncompressed points.** Indeed, we use compressed points in this EIP; when compressed points are used, any 32-byte string supplied by an attacker will resolve canonically to a point on the right curve, or else generate an error; there is no possibility of a "wrong curve" point. - **Only applies when you fail to check a point is on the curve.** But this is inapplicable for us anyway, since we use compressed points (see above). From 63011d318fa80dbfee8dde77e97cc5e42f578a18 Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 30 Dec 2022 05:48:25 -0700 Subject: [PATCH 096/274] Test cases: specify when contract creation fails in Valid initcode (#6237) --- EIPS/eip-3540.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index ef9e1997a7d189..038500049c4b23 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -265,13 +265,13 @@ All cases should be checked for creation transaction, `CREATE` and `CREATE2`. - Legacy init code - Returns legacy code - Returns valid EOF1 code - - Returns invalid EOF1 code - - Returns 0xEF not followed by EOF1 code + - Returns invalid EOF1 code, contract creation fails + - Returns 0xEF not followed by EOF1 code, contract creation fails - Valid EOF1 init code - - Returns legacy code + - Returns legacy code, contract creation fails - Returns valid EOF1 code - - Returns invalid EOF1 code - - Returns 0xEF not followed by EOF1 code + - Returns invalid EOF1 code, contract creation fails + - Returns 0xEF not followed by EOF1 code, contract creation fails - Invalid EOF1 init code ### Contract execution From 778ea2b5179c97d12b674d317ab9367ef9897efe Mon Sep 17 00:00:00 2001 From: Austin Zhu <42071208+AustinZhu@users.noreply.github.com> Date: Fri, 30 Dec 2022 23:00:27 +0900 Subject: [PATCH 097/274] Update EIP-5727: rename soul to owner; improve SlotEnumerable methods (#6240) * feat: upload eip draft * fix: update erc number * feat: upload assets * fix: update implementation link * fix: update author info * feat: upload sample implementation * fix: add hardhat project config * fix: revise eip-5727 * fix: change ERC to EIP * fix: fix links to eip * fix: update tests * feat: include interface id; update sample * fix: rename soul to owner; add methods to SlotEnumerable * fix: fix markdown lint problems --- EIPS/eip-5727.md | 179 +- assets/eip-5727/contracts/ERC5727.sol | 34 +- assets/eip-5727/contracts/ERC5727Delegate.sol | 12 +- .../eip-5727/contracts/ERC5727Enumerable.sol | 76 +- assets/eip-5727/contracts/ERC5727Example.sol | 30 +- .../eip-5727/contracts/ERC5727Governance.sol | 20 +- assets/eip-5727/contracts/ERC5727Recovery.sol | 8 +- assets/eip-5727/contracts/ERC5727Shadow.sol | 4 +- .../contracts/ERC5727SlotEnumerable.sol | 105 +- .../contracts/interfaces/IERC5727.sol | 20 +- .../contracts/interfaces/IERC5727Delegate.sol | 22 +- .../interfaces/IERC5727Enumerable.sol | 40 +- .../interfaces/IERC5727Governance.sol | 6 +- .../contracts/interfaces/IERC5727Recovery.sol | 8 +- .../interfaces/IERC5727SlotEnumerable.sol | 51 + assets/eip-5727/pnpm-lock.yaml | 4782 +++++++++++++++++ 16 files changed, 5186 insertions(+), 211 deletions(-) create mode 100644 assets/eip-5727/pnpm-lock.yaml diff --git a/EIPS/eip-5727.md b/EIPS/eip-5727.md index aae92a5f99355b..5784ebabfe8d1e 100644 --- a/EIPS/eip-5727.md +++ b/EIPS/eip-5727.md @@ -12,14 +12,16 @@ 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. + +An interface for soulbound tokens (SBT), which are non-transferable tokens representing a person's identity, credentials, affiliations, and reputation. 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. + +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 require 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. @@ -35,6 +37,7 @@ Our interface can be used to represent non-transferable ownerships, and provides 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. @@ -42,6 +45,7 @@ A token is identified by its `tokenId`, which is a 256-bit unsigned integer. A t 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 @@ -57,18 +61,18 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; interface IERC5727 is IERC165 { /** * @dev MUST emit when a token is minted. - * @param soul The address that the token is minted to + * @param owner 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); + event Minted(address indexed owner, uint256 indexed tokenId, uint256 value); /** * @dev MUST emit when a token is revoked. - * @param soul The owner soul of the revoked token + * @param owner The owner of the revoked token * @param tokenId The revoked token */ - event Revoked(address indexed soul, uint256 indexed tokenId); + event Revoked(address indexed owner, uint256 indexed tokenId); /** * @dev MUST emit when a token is charged. @@ -86,13 +90,14 @@ interface IERC5727 is IERC165 { /** * @dev MUST emit when a token is destroyed. - * @param soul The owner soul of the destroyed token + * @param owner The owner of the destroyed token * @param tokenId The token to destroy. */ - event Destroyed(address indexed soul, uint256 indexed tokenId); + event Destroyed(address indexed owner, uint256 indexed tokenId); /** * @dev MUST emit when the slot of a token is set or changed. + * @dev In case a new slot is set, the `oldSlot` MUST be 0. * @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 @@ -120,16 +125,17 @@ interface IERC5727 is IERC165 { function slotOf(uint256 tokenId) external view returns (uint256); /** - * @notice Get the owner soul of a token. + * @notice Get the owner 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` + * @param tokenId the token for which to query the owner + * @return The address of the owner of `tokenId` */ - function soulOf(uint256 tokenId) external view returns (address); + function ownerOf(uint256 tokenId) external view returns (address); /** * @notice Get the validity of a token. * @dev MUST revert if the `tokenId` does not exist + * @dev A token is valid if it is not revoked. * @param tokenId the token for which to query the validity * @return If the token is valid */ @@ -146,9 +152,12 @@ interface IERC5727 is IERC165 { ``` ### 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. + +This extension provides methods to enumerate the tokens of a owner. It is recommended to be implemented together with the core interface. ```solidity pragma solidity ^0.8.0; @@ -157,7 +166,7 @@ import "./IERC5727.sol"; /** * @title ERC5727 Soulbound Token Enumerable Interface - * @dev This extension allows querying the tokens of a soul. + * @dev This extension allows querying the tokens of a owner. * @dev interfaceId = 0x211ec300 */ interface IERC5727Enumerable is IERC5727 { @@ -168,19 +177,19 @@ interface IERC5727Enumerable is IERC5727 { function emittedCount() external view returns (uint256); /** - * @notice Get the total number of souls. - * @return The total number of souls + * @notice Get the total number of owners. + * @return The total number of owners */ - function soulsCount() external view returns (uint256); + function ownersCount() 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. + * @notice Get the tokenId with `index` of the `owner`. + * @dev MUST revert if the `index` exceed the number of tokens owned by the `owner`. + * @param owner The owner 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) + function tokenOfSoulByIndex(address owner, uint256 index) external view returns (uint256); @@ -194,23 +203,24 @@ interface IERC5727Enumerable is IERC5727 { 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` + * @notice Get the number of tokens owned by the `owner`. + * @dev MUST revert if the `owner` does not have any token. + * @param owner The owner whose balance is queried for + * @return The number of tokens of the `owner` */ - function balanceOf(address soul) external view returns (uint256); + function balanceOf(address owner) 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 + * @notice Get if the `owner` owns any valid tokens. + * @param owner The owner whose valid token information is queried for + * @return if the `owner` owns any valid tokens */ - function hasValid(address soul) external view returns (bool); + function hasValid(address owner) 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 @@ -261,6 +271,7 @@ interface IERC5727Metadata is IERC5727 { ``` #### 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 @@ -281,12 +292,12 @@ interface IERC5727Governance is IERC5727 { function voters() external view returns (address[] memory); /** - * @notice Approve to mint the token described by the `approvalRequestId` to `soul`. + * @notice Approve to mint the token described by the `approvalRequestId` to `owner`. * @dev MUST revert if the caller is not a voter. - * @param soul The soul which the token to mint to + * @param owner The owner 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; + function approveMint(address owner, uint256 approvalRequestId) external; /** * @notice Approve to revoke the `tokenId`. @@ -300,7 +311,7 @@ interface IERC5727Governance is IERC5727 { * @dev MUST revert when `value` is zero. * @param value The value of the approval request to create */ - function createApprovalRequest(uint256 value, uint256 slot) external; + function createApprovalRequest(uint256 value, uint256 slot) external returns (uint256 approvalRequestId); /** * @notice Remove `approvalRequestId` approval request. @@ -328,6 +339,7 @@ interface IERC5727Governance is IERC5727 { ``` #### 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 @@ -344,8 +356,8 @@ 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 + * @param operator The owner to which the minting right is delegated + * @param delegateRequestId The delegate request describing the owner, value and slot of the token to mint */ function mintDelegate(address operator, uint256 delegateRequestId) external; @@ -353,8 +365,8 @@ interface IERC5727Delegate is IERC5727 { * @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 + * @param operators The owners to which the minting right is delegated + * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint */ function mintDelegateBatch( address[] memory operators, @@ -364,7 +376,7 @@ interface IERC5727Delegate is IERC5727 { /** * @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 operator The owner to which the revoking right is delegated * @param tokenId The token to revoke */ function revokeDelegate(address operator, uint256 tokenId) external; @@ -373,7 +385,7 @@ interface IERC5727Delegate is IERC5727 { * @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 operators The owners to which the revoking right is delegated * @param tokenIds The tokens to revoke */ function revokeDelegateBatch( @@ -384,14 +396,14 @@ interface IERC5727Delegate is IERC5727 { /** * @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. + * @param delegateRequestId The delegate requests describing the owner, 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. + * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint. */ function delegateMintBatch(uint256[] memory delegateRequestIds) external; @@ -410,14 +422,14 @@ interface IERC5727Delegate is IERC5727 { 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. + * @notice Create a delegate request describing the `owner`, `value` and `slot` of a token. + * @param owner The owner 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, + address owner, uint256 value, uint256 slot ) external returns (uint256 delegateRequestId); @@ -433,7 +445,8 @@ interface IERC5727Delegate is IERC5727 { ``` #### 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. + +This extension provides methods to recover tokens from a stale owner. 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; @@ -447,15 +460,17 @@ import "./IERC5727.sol"; */ interface IERC5727Recovery is IERC5727 { /** - * @notice Recover the tokens of `soul` with `signature`. + * @notice Recover the tokens of `owner` 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` + * @param owner The owner whose tokens are recovered + * @param signature The signature signed by the `owner` */ - function recover(address soul, bytes memory signature) external; + function recover(address owner, 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 @@ -510,6 +525,7 @@ interface IERC5727Expirable is IERC5727 { ``` #### 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 @@ -540,6 +556,7 @@ interface IERC5727Shadow is IERC5727 { ``` #### SlotEnumerable + This extension provides methods to enumerate slots. A slot is used to group tokens that share similar utility and properties. ```solidity @@ -587,11 +604,64 @@ interface IERC5727SlotEnumerable is IERC5727, IERC5727Enumerable { external view returns (uint256); + + /** + * @notice Get the number of owners in a slot. + * @dev MUST revert if the slot does not exist. + * @param slot The slot whose number of owners is queried for + * @return The number of owners in the `slot` + */ + function ownersInSlot(uint256 slot) external view returns (uint256); + + /** + * @notice Check if a owner is in a slot. + * @dev MUST revert if the slot does not exist. + * @param owner The owner whose existence in the slot is queried for + * @param slot The slot whose existence of the owner is queried for + * @return True if the `owner` is in the `slot`, false otherwise + */ + function isOwnerInSlot( + address owner, + uint256 slot + ) external view returns (bool); + + /** + * @notice Get the owner with `index` of the `slot`. + * @dev MUST revert if the `index` exceed the number of owners in the `slot`. + * @param slot The slot whose owner is queried for. + * @param index The index of the owner queried for + * @return The owner is queried for + */ + function ownerInSlotByIndex( + uint256 slot, + uint256 index + ) external view returns (address); + + /** + * @notice Get the number of slots of a owner. + * @param owner The owner whose number of slots is queried for + * @return The number of slots of the `owner` + */ + function slotCountOfOwner(address owner) external view returns (uint256); + + /** + * @notice Get the slot with `index` of the `owner`. + * @dev MUST revert if the `index` exceed the number of slots of the `owner`. + * @param owner The owner whose slot is queried for. + * @param index The index of the slot queried for + * @return The slot is queried for + */ + function slotOfOwnerByIndex( + address owner, + 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. @@ -599,28 +669,35 @@ Firstly, each slot can be used to represent different categories of SBTs. For in 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). + +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 owner 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/assets/eip-5727/contracts/ERC5727.sol b/assets/eip-5727/contracts/ERC5727.sol index d3331759213584..ac3668a2097d34 100644 --- a/assets/eip-5727/contracts/ERC5727.sol +++ b/assets/eip-5727/contracts/ERC5727.sol @@ -24,7 +24,7 @@ abstract contract ERC5727 is struct Token { address issuer; - address soul; + address owner; bool valid; uint256 value; uint256 slot; @@ -60,37 +60,37 @@ abstract contract ERC5727 is returns (Token storage) { Token storage token = _tokens[tokenId]; - require(token.soul != address(0), "ERC5727: Token does not exist"); + require(token.owner != address(0), "ERC5727: Token does not exist"); return token; } function _mintUnsafe( - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, bool valid ) internal { - _mintUnsafe(_msgSender(), soul, tokenId, value, slot, valid); + _mintUnsafe(_msgSender(), owner, tokenId, value, slot, valid); } function _mintUnsafe( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, bool valid ) internal { require( - _tokens[tokenId].soul == address(0), + _tokens[tokenId].owner == 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); + _beforeTokenMint(issuer, owner, tokenId, value, slot, valid); + _tokens[tokenId] = Token(issuer, owner, valid, value, slot); + _afterTokenMint(issuer, owner, tokenId, value, slot, valid); + emit Minted(owner, tokenId, value); emit SlotChanged(tokenId, 0, slot); } @@ -142,7 +142,7 @@ abstract contract ERC5727 is _beforeTokenRevoke(tokenId); _tokens[tokenId].valid = false; _afterTokenRevoke(tokenId); - emit Revoked(_tokens[tokenId].soul, tokenId); + emit Revoked(_tokens[tokenId].owner, tokenId); } function _revokeBatch(uint256[] memory tokenIds) internal virtual { @@ -152,7 +152,7 @@ abstract contract ERC5727 is } function _destroy(uint256 tokenId) internal virtual { - address soul = soulOf(tokenId); + address owner = ownerOf(tokenId); uint256 slot = slotOf(tokenId); uint256 value = valueOf(tokenId); @@ -161,7 +161,7 @@ abstract contract ERC5727 is _afterTokenDestroy(tokenId); emit Consumed(tokenId, value); - emit Destroyed(soul, tokenId); + emit Destroyed(owner, tokenId); emit SlotChanged(tokenId, slot, 0); } @@ -173,7 +173,7 @@ abstract contract ERC5727 is function _beforeTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, @@ -182,7 +182,7 @@ abstract contract ERC5727 is function _afterTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, @@ -197,7 +197,7 @@ abstract contract ERC5727 is function _afterTokenDestroy(uint256 tokenId) internal virtual {} - function soulOf(uint256 tokenId) + function ownerOf(uint256 tokenId) public view virtual @@ -205,7 +205,7 @@ abstract contract ERC5727 is returns (address) { _beforeView(tokenId); - return _getTokenOrRevert(tokenId).soul; + return _getTokenOrRevert(tokenId).owner; } function valueOf(uint256 tokenId) diff --git a/assets/eip-5727/contracts/ERC5727Delegate.sol b/assets/eip-5727/contracts/ERC5727Delegate.sol index 36cbe90e5e3816..ff19ad40d2bbed 100644 --- a/assets/eip-5727/contracts/ERC5727Delegate.sol +++ b/assets/eip-5727/contracts/ERC5727Delegate.sol @@ -10,7 +10,7 @@ import "./ERC5727Enumerable.sol"; abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { struct DelegateRequest { - address soul; + address owner; uint256 value; uint256 slot; } @@ -86,7 +86,7 @@ abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { _mintAllowed[_msgSender()][delegateRequestId] = false; } _mint( - _delegateRequests[delegateRequestId].soul, + _delegateRequests[delegateRequestId].owner, _delegateRequests[delegateRequestId].value, _delegateRequests[delegateRequestId].slot ); @@ -192,7 +192,7 @@ abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { } function createDelegateRequest( - address soul, + address owner, uint256 value, uint256 slot ) external virtual override returns (uint256 delegateRequestId) { @@ -202,7 +202,7 @@ abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { "ERC5727Delegate: Value of Delegate Request cannot be zero" ); delegateRequestId = _delegateRequestCount; - _delegateRequests[_delegateRequestCount].soul = soul; + _delegateRequests[_delegateRequestCount].owner = owner; _delegateRequests[_delegateRequestCount].value = value; _delegateRequests[_delegateRequestCount].slot = slot; _delegateRequestCount++; @@ -235,7 +235,7 @@ abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { return _delegatedTokens[operator].values(); } - function soulOfDelegateRequest(uint256 delegateRequestId) + function ownerOfDelegateRequest(uint256 delegateRequestId) public view virtual @@ -245,7 +245,7 @@ abstract contract ERC5727Delegate is ERC5727Enumerable, IERC5727Delegate { delegateRequestId < _delegateRequestCount, "ERC5727Delegate: Delegate request does not exist" ); - return _delegateRequests[delegateRequestId].soul; + return _delegateRequests[delegateRequestId].owner; } function valueOfDelegateRequest(uint256 delegateRequestId) diff --git a/assets/eip-5727/contracts/ERC5727Enumerable.sol b/assets/eip-5727/contracts/ERC5727Enumerable.sol index 9d76812451b159..33453497db6cfa 100644 --- a/assets/eip-5727/contracts/ERC5727Enumerable.sol +++ b/assets/eip-5727/contracts/ERC5727Enumerable.sol @@ -15,7 +15,7 @@ abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { mapping(address => uint256) private _numberOfValidTokens; Counters.Counter private _emittedCount; - Counters.Counter private _soulsCount; + Counters.Counter private _ownersCount; function supportsInterface(bytes4 interfaceId) public @@ -31,14 +31,14 @@ abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { function _beforeTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, bool valid ) internal virtual override { - if (_indexedTokenIds[soul].length() == 0) { - _soulsCount.increment(); + if (_indexedTokenIds[owner].length() == 0) { + _ownersCount.increment(); } //unused variables issuer; @@ -50,15 +50,15 @@ abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { function _afterTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, bool valid ) internal virtual override { - _indexedTokenIds[soul].add(tokenId); + _indexedTokenIds[owner].add(tokenId); if (valid) { - _numberOfValidTokens[soul] += 1; + _numberOfValidTokens[owner] += 1; } //unused variables issuer; @@ -68,55 +68,55 @@ abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { } function _mint( - address soul, + address owner, uint256 value, uint256 slot ) internal virtual returns (uint256 tokenId) { tokenId = _emittedCount.current(); - _mintUnsafe(soul, tokenId, value, slot, true); - emit Minted(soul, tokenId, value); + _mintUnsafe(owner, tokenId, value, slot, true); + emit Minted(owner, tokenId, value); _emittedCount.increment(); } function _mint( address issuer, - address soul, + address owner, uint256 value, uint256 slot ) internal virtual returns (uint256 tokenId) { tokenId = _emittedCount.current(); - _mintUnsafe(issuer, soul, tokenId, value, slot, true); - emit Minted(soul, tokenId, value); + _mintUnsafe(issuer, owner, tokenId, value, slot, true); + emit Minted(owner, tokenId, value); _emittedCount.increment(); } function _mintBatch( - address[] memory souls, + address[] memory owners, 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); + tokenIds = new uint256[](owners.length); + for (uint256 i = 0; i < owners.length; i++) { + tokenIds[i] = _mint(owners[i], value, slot); } } function _afterTokenRevoke(uint256 tokenId) internal virtual override { - assert(_numberOfValidTokens[_getTokenOrRevert(tokenId).soul] > 0); - _numberOfValidTokens[_getTokenOrRevert(tokenId).soul] -= 1; + assert(_numberOfValidTokens[_getTokenOrRevert(tokenId).owner] > 0); + _numberOfValidTokens[_getTokenOrRevert(tokenId).owner] -= 1; } function _beforeTokenDestroy(uint256 tokenId) internal virtual override { - address soul = soulOf(tokenId); + address owner = ownerOf(tokenId); if (_getTokenOrRevert(tokenId).valid) { - assert(_numberOfValidTokens[soul] > 0); - _numberOfValidTokens[soul] -= 1; + assert(_numberOfValidTokens[owner] > 0); + _numberOfValidTokens[owner] -= 1; } - EnumerableSet.remove(_indexedTokenIds[soul], tokenId); - if (EnumerableSet.length(_indexedTokenIds[soul]) == 0) { - assert(_soulsCount.current() > 0); - _soulsCount.decrement(); + EnumerableSet.remove(_indexedTokenIds[owner], tokenId); + if (EnumerableSet.length(_indexedTokenIds[owner]) == 0) { + assert(_ownersCount.current() > 0); + _ownersCount.decrement(); } } @@ -124,51 +124,51 @@ abstract contract ERC5727Enumerable is ERC5727, IERC5727Enumerable { _emittedCount.increment(); } - function _tokensOfSoul(address soul) + function _tokensOfOwner(address owner) internal view returns (uint256[] memory tokenIds) { - tokenIds = _indexedTokenIds[soul].values(); - require(tokenIds.length != 0, "ERC5727: the soul has no token"); + tokenIds = _indexedTokenIds[owner].values(); + require(tokenIds.length != 0, "ERC5727: the owner has no token"); } - function emittedCount() public view virtual override returns (uint256) { + function totalSupply() public view virtual override returns (uint256) { return _emittedCount.current(); } - function soulsCount() public view virtual override returns (uint256) { - return _soulsCount.current(); + function ownersCount() public view virtual override returns (uint256) { + return _ownersCount.current(); } - function balanceOf(address soul) + function balanceOf(address owner) public view virtual override returns (uint256) { - return _indexedTokenIds[soul].length(); + return _indexedTokenIds[owner].length(); } - function hasValid(address soul) + function hasValid(address owner) public view virtual override returns (bool) { - return _numberOfValidTokens[soul] > 0; + return _numberOfValidTokens[owner] > 0; } - function tokenOfSoulByIndex(address soul, uint256 index) + function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { - EnumerableSet.UintSet storage ids = _indexedTokenIds[soul]; + EnumerableSet.UintSet storage ids = _indexedTokenIds[owner]; require( index < EnumerableSet.length(ids), "ERC5727: Token does not exist" diff --git a/assets/eip-5727/contracts/ERC5727Example.sol b/assets/eip-5727/contracts/ERC5727Example.sol index f357b05cd1fbf7..35ae865fc9a8f9 100644 --- a/assets/eip-5727/contracts/ERC5727Example.sol +++ b/assets/eip-5727/contracts/ERC5727Example.sol @@ -59,13 +59,13 @@ contract ERC5727Example is } function mint( - address soul, + address owner, uint256 value, uint256 slot, uint256 expiryDate, bool shadowed ) public virtual onlyOwner { - uint256 tokenId = _mint(soul, value, slot); + uint256 tokenId = _mint(owner, value, slot); if (shadowed) { _shadow(tokenId); } @@ -77,13 +77,13 @@ contract ERC5727Example is } function mintBatch( - address[] memory souls, + address[] memory owners, uint256 value, uint256 slot, uint256 expiryDate, bool shadowed ) public virtual onlyOwner { - uint256[] memory tokenIds = _mintBatch(souls, value, slot); + uint256[] memory tokenIds = _mintBatch(owners, value, slot); for (uint256 i = 0; i < tokenIds.length; i++) { if (shadowed) _shadow(tokenIds[i]); _setExpiryDate(tokenIds[i], expiryDate); @@ -96,7 +96,7 @@ contract ERC5727Example is function _beforeTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, @@ -108,7 +108,7 @@ contract ERC5727Example is { ERC5727Enumerable._beforeTokenMint( issuer, - soul, + owner, tokenId, value, slot, @@ -116,7 +116,7 @@ contract ERC5727Example is ); ERC5727SlotEnumerable._beforeTokenMint( issuer, - soul, + owner, tokenId, value, slot, @@ -126,7 +126,7 @@ contract ERC5727Example is function _afterTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, @@ -134,7 +134,7 @@ contract ERC5727Example is ) internal virtual override(ERC5727, ERC5727Enumerable) { ERC5727Enumerable._afterTokenMint( issuer, - soul, + owner, tokenId, value, slot, @@ -158,4 +158,16 @@ contract ERC5727Example is ERC5727Enumerable._beforeTokenDestroy(tokenId); ERC5727SlotEnumerable._beforeTokenDestroy(tokenId); } + + function slotURI( + uint256 slot + ) + public + view + virtual + override(ERC5727, ERC5727SlotEnumerable) + returns (string memory) + { + return ERC5727SlotEnumerable.slotURI(slot); + } } diff --git a/assets/eip-5727/contracts/ERC5727Governance.sol b/assets/eip-5727/contracts/ERC5727Governance.sol index 3c10bbe1ce9726..972f6a4c1dfa69 100644 --- a/assets/eip-5727/contracts/ERC5727Governance.sol +++ b/assets/eip-5727/contracts/ERC5727Governance.sol @@ -50,26 +50,26 @@ abstract contract ERC5727Governance is ERC5727Enumerable, IERC5727Governance { return _votersArray.values(); } - function approveMint(address soul, uint256 approvalRequestId) + function approveMint(address owner, uint256 approvalRequestId) public virtual override onlyRole(VOTER_ROLE) { require( - !_mintApprovals[_msgSender()][approvalRequestId][soul], + !_mintApprovals[_msgSender()][approvalRequestId][owner], "ERC5727Governance: You already approved this address" ); - _mintApprovals[_msgSender()][approvalRequestId][soul] = true; - _mintApprovalCounts[approvalRequestId][soul] += 1; + _mintApprovals[_msgSender()][approvalRequestId][owner] = true; + _mintApprovalCounts[approvalRequestId][owner] += 1; if ( - _mintApprovalCounts[approvalRequestId][soul] == + _mintApprovalCounts[approvalRequestId][owner] == _votersArray.length() ) { - _resetMintApprovals(approvalRequestId, soul); + _resetMintApprovals(approvalRequestId, owner); _mint( _approvalRequests[approvalRequestId].creator, - soul, + owner, _approvalRequests[approvalRequestId].value, _approvalRequests[approvalRequestId].slot ); @@ -106,13 +106,13 @@ abstract contract ERC5727Governance is ERC5727Enumerable, IERC5727Governance { super.supportsInterface(interfaceId); } - function _resetMintApprovals(uint256 approvalRequestId, address soul) + function _resetMintApprovals(uint256 approvalRequestId, address owner) private { for (uint256 i = 0; i < _votersArray.length(); i++) { - _mintApprovals[_votersArray.at(i)][approvalRequestId][soul] = false; + _mintApprovals[_votersArray.at(i)][approvalRequestId][owner] = false; } - _mintApprovalCounts[approvalRequestId][soul] = 0; + _mintApprovalCounts[approvalRequestId][owner] = 0; } function _resetRevokeApprovals(uint256 tokenId) private { diff --git a/assets/eip-5727/contracts/ERC5727Recovery.sol b/assets/eip-5727/contracts/ERC5727Recovery.sol index bf679ce63f36cd..88ff487c2990ef 100644 --- a/assets/eip-5727/contracts/ERC5727Recovery.sol +++ b/assets/eip-5727/contracts/ERC5727Recovery.sol @@ -10,16 +10,16 @@ import "./ERC5727Enumerable.sol"; abstract contract ERC5727Recovery is ERC5727Enumerable, IERC5727Recovery { using ECDSA for bytes32; - function recover(address soul, bytes memory signature) + function recover(address owner, bytes memory signature) public virtual override { address recipient = _msgSender(); - bytes32 messageHash = keccak256(abi.encodePacked(soul, recipient)); + bytes32 messageHash = keccak256(abi.encodePacked(owner, recipient)); bytes32 signedHash = messageHash.toEthSignedMessageHash(); - require(signedHash.recover(signature) == soul, "Invalid signature"); - uint256[] memory tokenIds = _tokensOfSoul(soul); + require(signedHash.recover(signature) == owner, "Invalid signature"); + uint256[] memory tokenIds = _tokensOfOwner(owner); for (uint256 i = 0; i < tokenIds.length; i++) { Token storage token = _getTokenOrRevert(tokenIds[i]); address issuer = token.issuer; diff --git a/assets/eip-5727/contracts/ERC5727Shadow.sol b/assets/eip-5727/contracts/ERC5727Shadow.sol index 176d9bd485063a..d5ef9c138ad09a 100644 --- a/assets/eip-5727/contracts/ERC5727Shadow.sol +++ b/assets/eip-5727/contracts/ERC5727Shadow.sol @@ -9,7 +9,7 @@ abstract contract ERC5727Shadow is ERC5727, IERC5727Shadow { modifier onlyManager(uint256 tokenId) { require( - _msgSender() == _getTokenOrRevert(tokenId).soul || + _msgSender() == _getTokenOrRevert(tokenId).owner || _msgSender() == _getTokenOrRevert(tokenId).issuer, "ERC5727Shadow: You are not the manager" ); @@ -19,7 +19,7 @@ abstract contract ERC5727Shadow is ERC5727, IERC5727Shadow { function _beforeView(uint256 tokenId) internal view virtual override { require( !_shadowed[tokenId] || - _msgSender() == _getTokenOrRevert(tokenId).soul || + _msgSender() == _getTokenOrRevert(tokenId).owner || _msgSender() == _getTokenOrRevert(tokenId).issuer, "ERC5727Shadow: the token is shadowed" ); diff --git a/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol b/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol index 6dc36723cc60db..b9f6b982a9c6d7 100644 --- a/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol +++ b/assets/eip-5727/contracts/ERC5727SlotEnumerable.sol @@ -1,4 +1,4 @@ -//SPDX-License-Identifier: CC0-1.0 +//SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -8,9 +8,14 @@ import "./interfaces/IERC5727SlotEnumerable.sol"; abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { using EnumerableSet for EnumerableSet.UintSet; + using EnumerableSet for EnumerableSet.AddressSet; mapping(uint256 => EnumerableSet.UintSet) private _tokensInSlot; + mapping(uint256 => EnumerableSet.AddressSet) private _ownersInSlot; + + mapping(address => EnumerableSet.UintSet) private _slotsOfOwner; + EnumerableSet.UintSet private _allSlots; function slotCount() public view override returns (uint256) { @@ -29,24 +34,19 @@ abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { return _allSlots.length() != 0 && _allSlots.contains(slot); } - function tokenSupplyInSlot(uint256 slot) - public - view - override - returns (uint256) - { + 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) - { + function tokenInSlotByIndex( + uint256 slot, + uint256 index + ) public view override returns (uint256) { require( index < ERC5727SlotEnumerable.tokenSupplyInSlot(slot), "ERC5727SlotEnumerable: slot token index out of bounds" @@ -54,13 +54,51 @@ abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { return _tokensInSlot[slot].at(index); } - function supportsInterface(bytes4 interfaceId) - public - view - virtual - override(IERC165, ERC5727) - returns (bool) - { + function ownersInSlot(uint256 slot) public view override returns (uint256) { + if (!_slotExists(slot)) { + return 0; + } + return _ownersInSlot[slot].length(); + } + + function ownerInSlotByIndex( + uint256 slot, + uint256 index + ) public view override returns (address) { + require( + index < ERC5727SlotEnumerable.ownersInSlot(slot), + "ERC5727SlotEnumerable: slot owner index out of bounds" + ); + return _ownersInSlot[slot].at(index); + } + + function slotCountOfOwner( + address owner + ) public view override returns (uint256) { + return _slotsOfOwner[owner].length(); + } + + function slotOfOwnerByIndex( + address owner, + uint256 index + ) public view override returns (uint256) { + require( + index < ERC5727SlotEnumerable.slotCountOfOwner(owner), + "ERC5727SlotEnumerable: owner slot index out of bounds" + ); + return _slotsOfOwner[owner].at(index); + } + + function isOwnerInSlot( + address owner, + uint256 slot + ) public view virtual override returns (bool) { + return _ownersInSlot[slot].contains(owner); + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC5727) returns (bool) { return interfaceId == type(IERC5727SlotEnumerable).interfaceId || super.supportsInterface(interfaceId); @@ -68,7 +106,7 @@ abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { function _beforeTokenMint( address issuer, - address soul, + address owner, uint256 tokenId, uint256 value, uint256 slot, @@ -78,11 +116,16 @@ abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { _allSlots.add(slot); } _tokensInSlot[slot].add(tokenId); - //unused - issuer; - soul; - value; - valid; + if (!_ownersInSlot[slot].contains(owner)) { + _ownersInSlot[slot].add(owner); + } + _slotsOfOwner[owner].add(slot); + } + + function _addSlot(uint256 slot) internal virtual { + if (!_slotExists(slot)) { + _allSlots.add(slot); + } } function _beforeTokenDestroy(uint256 tokenId) internal virtual override { @@ -92,4 +135,14 @@ abstract contract ERC5727SlotEnumerable is ERC5727, IERC5727SlotEnumerable { _allSlots.remove(slot); } } + + function slotURI( + uint256 slot + ) public view virtual override returns (string memory) { + require( + _slotExists(slot), + "ERC5727SlotEnumerable: slot does not exist" + ); + return super.slotURI(slot); + } } diff --git a/assets/eip-5727/contracts/interfaces/IERC5727.sol b/assets/eip-5727/contracts/interfaces/IERC5727.sol index dddfcce32c53e9..0e013ed2be3999 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727.sol @@ -10,18 +10,18 @@ import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; interface IERC5727 is IERC165 { /** * @dev MUST emit when a token is minted. - * @param soul The address that the token is minted to + * @param owner 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); + event Minted(address indexed owner, uint256 indexed tokenId, uint256 value); /** * @dev MUST emit when a token is revoked. - * @param soul The owner soul of the revoked token + * @param owner The owner of the revoked token * @param tokenId The revoked token */ - event Revoked(address indexed soul, uint256 indexed tokenId); + event Revoked(address indexed owner, uint256 indexed tokenId); /** * @dev MUST emit when a token is charged. @@ -39,10 +39,10 @@ interface IERC5727 is IERC165 { /** * @dev MUST emit when a token is destroyed. - * @param soul The owner soul of the destroyed token + * @param owner The owner of the destroyed token * @param tokenId The token to destroy. */ - event Destroyed(address indexed soul, uint256 indexed tokenId); + event Destroyed(address indexed owner, uint256 indexed tokenId); /** * @dev MUST emit when the slot of a token is set or changed. @@ -73,12 +73,12 @@ interface IERC5727 is IERC165 { function slotOf(uint256 tokenId) external view returns (uint256); /** - * @notice Get the owner soul of a token. + * @notice Get the owner 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` + * @param tokenId the token for which to query the owner + * @return The address of the owner of `tokenId` */ - function soulOf(uint256 tokenId) external view returns (address); + function ownerOf(uint256 tokenId) external view returns (address); /** * @notice Get the validity of a token. diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol b/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol index eeeeecdb9dc59f..397fc231477080 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727Delegate.sol @@ -12,8 +12,8 @@ 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 + * @param operator The owner to which the minting right is delegated + * @param delegateRequestId The delegate request describing the owner, value and slot of the token to mint */ function mintDelegate(address operator, uint256 delegateRequestId) external; @@ -21,8 +21,8 @@ interface IERC5727Delegate is IERC5727 { * @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 + * @param operators The owners to which the minting right is delegated + * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint */ function mintDelegateBatch( address[] memory operators, @@ -32,7 +32,7 @@ interface IERC5727Delegate is IERC5727 { /** * @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 operator The owner to which the revoking right is delegated * @param tokenId The token to revoke */ function revokeDelegate(address operator, uint256 tokenId) external; @@ -41,7 +41,7 @@ interface IERC5727Delegate is IERC5727 { * @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 operators The owners to which the revoking right is delegated * @param tokenIds The tokens to revoke */ function revokeDelegateBatch( @@ -52,14 +52,14 @@ interface IERC5727Delegate is IERC5727 { /** * @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. + * @param delegateRequestId The delegate requests describing the owner, 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. + * @param delegateRequestIds The delegate requests describing the owner, value and slot of the tokens to mint. */ function delegateMintBatch(uint256[] memory delegateRequestIds) external; @@ -78,14 +78,14 @@ interface IERC5727Delegate is IERC5727 { 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. + * @notice Create a delegate request describing the `owner`, `value` and `slot` of a token. + * @param owner The owner 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, + address owner, uint256 value, uint256 slot ) external returns (uint256 delegateRequestId); diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol b/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol index a0a5f9e36006c4..b07ef309d346f2 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727Enumerable.sol @@ -5,29 +5,29 @@ import "./IERC5727.sol"; /** * @title ERC5727 Soulbound Token Enumerable Interface - * @dev This extension allows querying the tokens of a soul. + * @dev This extension allows querying the tokens of a owner. */ interface IERC5727Enumerable is IERC5727 { /** - * @notice Get the total number of tokens emitted. - * @return The total number of tokens emitted + * @notice Get the total number of tokens tracked by this contract. + * @return The total number of tokens tracked by this contract */ - function emittedCount() external view returns (uint256); + function totalSupply() external view returns (uint256); /** - * @notice Get the total number of souls. - * @return The total number of souls + * @notice Get the total number of owners. + * @return The total number of owners */ - function soulsCount() external view returns (uint256); + function ownersCount() 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. + * @notice Get the tokenId with `index` of the `owner`. + * @dev MUST revert if the `index` exceed the number of tokens owned by the `owner`. + * @param owner The owner 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) + function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256); @@ -41,17 +41,17 @@ interface IERC5727Enumerable is IERC5727 { 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` + * @notice Get the number of tokens owned by the `owner`. + * @dev MUST revert if the `owner` does not have any token. + * @param owner The owner whose balance is queried for + * @return The number of tokens of the `owner` */ - function balanceOf(address soul) external view returns (uint256); + function balanceOf(address owner) 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 + * @notice Get if the `owner` owns any valid tokens. + * @param owner The owner whose valid token infomation is queried for + * @return if the `owner` owns any valid tokens */ - function hasValid(address soul) external view returns (bool); + function hasValid(address owner) external view returns (bool); } diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol b/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol index 72a173edd7ebc2..ffa6b82c7b4d19 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727Governance.sol @@ -15,12 +15,12 @@ interface IERC5727Governance is IERC5727 { function voters() external view returns (address[] memory); /** - * @notice Approve to mint the token described by the `approvalRequestId` to `soul`. + * @notice Approve to mint the token described by the `approvalRequestId` to `owner`. * @dev MUST revert if the caller is not a voter. - * @param soul The soul which the token to mint to + * @param owner The owner 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; + function approveMint(address owner, uint256 approvalRequestId) external; /** * @notice Approve to revoke the `tokenId`. diff --git a/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol b/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol index ce715d07251732..7ed01170a523af 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727Recovery.sol @@ -9,10 +9,10 @@ import "./IERC5727.sol"; */ interface IERC5727Recovery is IERC5727 { /** - * @notice Recover the tokens of `soul` with `signature`. + * @notice Recover the tokens of `owner` 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` + * @param owner The owner whose tokens are recovered + * @param signature The signature signed by the `owner` */ - function recover(address soul, bytes memory signature) external; + function recover(address owner, bytes memory signature) external; } diff --git a/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol b/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol index fa224bcb8d1662..1fe889a6a991d8 100644 --- a/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol +++ b/assets/eip-5727/contracts/interfaces/IERC5727SlotEnumerable.sol @@ -42,4 +42,55 @@ interface IERC5727SlotEnumerable is IERC5727, IERC5727Enumerable { external view returns (uint256); + + /** + * @notice Get the number of owners in a slot. + * @dev MUST revert if the slot does not exist. + * @param slot The slot whose number of owners is queried for + * @return The number of owners in the `slot` + */ + function ownersInSlot(uint256 slot) external view returns (uint256); + + /** + * @notice Check if a owner is in a slot. + * @dev MUST revert if the slot does not exist. + * @param owner The owner whose existence in the slot is queried for + * @param slot The slot whose existence of the owner is queried for + * @return True if the `owner` is in the `slot`, false otherwise + */ + function isOwnerInSlot( + address owner, + uint256 slot + ) external view returns (bool); + + /** + * @notice Get the owner with `index` of the `slot`. + * @dev MUST revert if the `index` exceed the number of owners in the `slot`. + * @param slot The slot whose owner is queried for. + * @param index The index of the owner queried for + * @return The owner is queried for + */ + function ownerInSlotByIndex( + uint256 slot, + uint256 index + ) external view returns (address); + + /** + * @notice Get the number of slots of a owner. + * @param owner The owner whose number of slots is queried for + * @return The number of slots of the `owner` + */ + function slotCountOfOwner(address owner) external view returns (uint256); + + /** + * @notice Get the slot with `index` of the `owner`. + * @dev MUST revert if the `index` exceed the number of slots of the `owner`. + * @param owner The owner whose slot is queried for. + * @param index The index of the slot queried for + * @return The slot is queried for + */ + function slotOfOwnerByIndex( + address owner, + uint256 index + ) external view returns (uint256); } diff --git a/assets/eip-5727/pnpm-lock.yaml b/assets/eip-5727/pnpm-lock.yaml new file mode 100644 index 00000000000000..b01e41c47c3ace --- /dev/null +++ b/assets/eip-5727/pnpm-lock.yaml @@ -0,0 +1,4782 @@ +lockfileVersion: 5.4 + +specifiers: + '@nomicfoundation/hardhat-toolbox': 2.0.0 + '@openzeppelin/contracts': 4.7.3 + '@typechain/hardhat': 6.1.3 + '@types/node': 18.7.13 + hardhat: 2.11.2 + ts-node: 10.9.1 + typescript: 4.8.4 + +dependencies: + '@openzeppelin/contracts': 4.7.3 + +devDependencies: + '@nomicfoundation/hardhat-toolbox': 2.0.0_epcfcfx6issnt2xkawqsyw6j2u + '@typechain/hardhat': 6.1.3_hdc77mquumuyu4r5wip4q5ohha + '@types/node': 18.7.13 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + ts-node: 10.9.1_ieummqxttktzud32hpyrer46t4 + typescript: 4.8.4 + +packages: + + /@cspotcode/source-map-support/0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@ethersproject/abi/5.7.0: + resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==} + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + dev: true + + /@ethersproject/abstract-provider/5.7.0: + resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==} + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 + dev: true + + /@ethersproject/abstract-signer/5.7.0: + resolution: {integrity: sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==} + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + dev: true + + /@ethersproject/address/5.7.0: + resolution: {integrity: sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==} + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/rlp': 5.7.0 + dev: true + + /@ethersproject/base64/5.7.0: + resolution: {integrity: sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==} + dependencies: + '@ethersproject/bytes': 5.7.0 + dev: true + + /@ethersproject/basex/5.7.0: + resolution: {integrity: sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/properties': 5.7.0 + dev: true + + /@ethersproject/bignumber/5.7.0: + resolution: {integrity: sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + bn.js: 5.2.1 + dev: true + + /@ethersproject/bytes/5.7.0: + resolution: {integrity: sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==} + dependencies: + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/constants/5.7.0: + resolution: {integrity: sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==} + dependencies: + '@ethersproject/bignumber': 5.7.0 + dev: true + + /@ethersproject/contracts/5.7.0: + resolution: {integrity: sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==} + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/transactions': 5.7.0 + dev: true + + /@ethersproject/hash/5.7.0: + resolution: {integrity: sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==} + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + dev: true + + /@ethersproject/hdnode/5.7.0: + resolution: {integrity: sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==} + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 + dev: true + + /@ethersproject/json-wallets/5.7.0: + resolution: {integrity: sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==} + dependencies: + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + aes-js: 3.0.0 + scrypt-js: 3.0.1 + dev: true + + /@ethersproject/keccak256/5.7.0: + resolution: {integrity: sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==} + dependencies: + '@ethersproject/bytes': 5.7.0 + js-sha3: 0.8.0 + dev: true + + /@ethersproject/logger/5.7.0: + resolution: {integrity: sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==} + dev: true + + /@ethersproject/networks/5.7.1: + resolution: {integrity: sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==} + dependencies: + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/pbkdf2/5.7.0: + resolution: {integrity: sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/sha2': 5.7.0 + dev: true + + /@ethersproject/properties/5.7.0: + resolution: {integrity: sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==} + dependencies: + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/providers/5.7.1: + resolution: {integrity: sha512-vZveG/DLyo+wk4Ga1yx6jSEHrLPgmTt+dFv0dv8URpVCRf0jVhalps1jq/emN/oXnMRsC7cQgAF32DcXLL7BPQ==} + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/web': 5.7.1 + bech32: 1.1.4 + ws: 7.4.6 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /@ethersproject/random/5.7.0: + resolution: {integrity: sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/rlp/5.7.0: + resolution: {integrity: sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/sha2/5.7.0: + resolution: {integrity: sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + hash.js: 1.1.7 + dev: true + + /@ethersproject/signing-key/5.7.0: + resolution: {integrity: sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + bn.js: 5.2.1 + elliptic: 6.5.4 + hash.js: 1.1.7 + dev: true + + /@ethersproject/solidity/5.7.0: + resolution: {integrity: sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==} + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/strings': 5.7.0 + dev: true + + /@ethersproject/strings/5.7.0: + resolution: {integrity: sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/transactions/5.7.0: + resolution: {integrity: sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==} + dependencies: + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + dev: true + + /@ethersproject/units/5.7.0: + resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==} + dependencies: + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/logger': 5.7.0 + dev: true + + /@ethersproject/wallet/5.7.0: + resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==} + dependencies: + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/json-wallets': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/random': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/wordlists': 5.7.0 + dev: true + + /@ethersproject/web/5.7.1: + resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==} + dependencies: + '@ethersproject/base64': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + dev: true + + /@ethersproject/wordlists/5.7.0: + resolution: {integrity: sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==} + dependencies: + '@ethersproject/bytes': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/strings': 5.7.0 + dev: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@metamask/eth-sig-util/4.0.1: + resolution: {integrity: sha512-tghyZKLHZjcdlDqCA3gNZmLeR0XvOE9U1qoQO9ohyAZT6Pya+H9vkBPcsyXytmYLNgVoin7CKCmweo/R43V+tQ==} + engines: {node: '>=12.0.0'} + dependencies: + ethereumjs-abi: 0.6.8 + ethereumjs-util: 6.2.1 + ethjs-util: 0.1.6 + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + dev: true + + /@noble/hashes/1.1.2: + resolution: {integrity: sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==} + dev: true + + /@noble/secp256k1/1.6.3: + resolution: {integrity: sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==} + dev: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + + /@nomicfoundation/ethereumjs-block/4.0.0: + resolution: {integrity: sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-trie': 5.0.0 + '@nomicfoundation/ethereumjs-tx': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + ethereum-cryptography: 0.1.3 + dev: true + + /@nomicfoundation/ethereumjs-blockchain/6.0.0: + resolution: {integrity: sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-block': 4.0.0 + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-ethash': 2.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-trie': 5.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + abstract-level: 1.0.3 + debug: 4.3.4 + ethereum-cryptography: 0.1.3 + level: 8.0.0 + lru-cache: 5.1.1 + memory-level: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@nomicfoundation/ethereumjs-common/3.0.0: + resolution: {integrity: sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==} + dependencies: + '@nomicfoundation/ethereumjs-util': 8.0.0 + crc-32: 1.2.2 + dev: true + + /@nomicfoundation/ethereumjs-ethash/2.0.0: + resolution: {integrity: sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-block': 4.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + abstract-level: 1.0.3 + bigint-crypto-utils: 3.1.7 + ethereum-cryptography: 0.1.3 + dev: true + + /@nomicfoundation/ethereumjs-evm/1.0.0: + resolution: {integrity: sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + '@types/async-eventemitter': 0.2.1 + async-eventemitter: 0.2.4 + debug: 4.3.4 + ethereum-cryptography: 0.1.3 + mcl-wasm: 0.7.9 + rustbn.js: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@nomicfoundation/ethereumjs-rlp/4.0.0: + resolution: {integrity: sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /@nomicfoundation/ethereumjs-statemanager/1.0.0: + resolution: {integrity: sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==} + dependencies: + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-trie': 5.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + debug: 4.3.4 + ethereum-cryptography: 0.1.3 + functional-red-black-tree: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@nomicfoundation/ethereumjs-trie/5.0.0: + resolution: {integrity: sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + ethereum-cryptography: 0.1.3 + readable-stream: 3.6.0 + dev: true + + /@nomicfoundation/ethereumjs-tx/4.0.0: + resolution: {integrity: sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + ethereum-cryptography: 0.1.3 + dev: true + + /@nomicfoundation/ethereumjs-util/8.0.0: + resolution: {integrity: sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + ethereum-cryptography: 0.1.3 + dev: true + + /@nomicfoundation/ethereumjs-vm/6.0.0: + resolution: {integrity: sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==} + engines: {node: '>=14'} + dependencies: + '@nomicfoundation/ethereumjs-block': 4.0.0 + '@nomicfoundation/ethereumjs-blockchain': 6.0.0 + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-evm': 1.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-statemanager': 1.0.0 + '@nomicfoundation/ethereumjs-trie': 5.0.0 + '@nomicfoundation/ethereumjs-tx': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + '@types/async-eventemitter': 0.2.1 + async-eventemitter: 0.2.4 + debug: 4.3.4 + ethereum-cryptography: 0.1.3 + functional-red-black-tree: 1.0.1 + mcl-wasm: 0.7.9 + rustbn.js: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@nomicfoundation/hardhat-chai-matchers/1.0.3_u4bbm4ajgevldrm2ufutb7vine: + resolution: {integrity: sha512-qEE7Drs2HSY+krH09TXm6P9LFogs0BqbUq6wPD7nQRhmJ+p5zoDaIZjM5WL1pHqU5MpGqya3y+BdwmTYBfU5UA==} + peerDependencies: + '@nomiclabs/hardhat-ethers': ^2.0.0 + chai: ^4.2.0 + ethers: ^5.0.0 + hardhat: ^2.9.4 + dependencies: + '@ethersproject/abi': 5.7.0 + '@nomiclabs/hardhat-ethers': 2.1.1_liuail6phkx7un26teqxcf6yx4 + '@types/chai-as-promised': 7.1.5 + chai: 4.3.6 + chai-as-promised: 7.1.1_chai@4.3.6 + chalk: 2.4.2 + deep-eql: 4.1.1 + ethers: 5.7.1 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + ordinal: 1.0.3 + dev: true + + /@nomicfoundation/hardhat-network-helpers/1.0.6_hardhat@2.11.2: + resolution: {integrity: sha512-a35iVD4ycF6AoTfllAnKm96IPIzzHpgKX/ep4oKc2bsUKFfMlacWdyntgC/7d5blyCTXfFssgNAvXDZfzNWVGQ==} + peerDependencies: + hardhat: ^2.9.5 + dependencies: + ethereumjs-util: 7.1.5 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + dev: true + + /@nomicfoundation/hardhat-toolbox/2.0.0_epcfcfx6issnt2xkawqsyw6j2u: + resolution: {integrity: sha512-BoOPbzLQ1GArnBZd4Jz4IU8FY3RY4nUwpXlfymXwxlXNimngkPRJj7ivVNurD7igohEjf90v/Axn2M5WwAdCJQ==} + peerDependencies: + '@ethersproject/abi': ^5.4.7 + '@ethersproject/providers': ^5.4.7 + '@nomicfoundation/hardhat-chai-matchers': ^1.0.0 + '@nomicfoundation/hardhat-network-helpers': ^1.0.0 + '@nomiclabs/hardhat-ethers': ^2.0.0 + '@nomiclabs/hardhat-etherscan': ^3.0.0 + '@typechain/ethers-v5': ^10.1.0 + '@typechain/hardhat': ^6.1.2 + '@types/chai': ^4.2.0 + '@types/mocha': ^9.1.0 + '@types/node': '>=12.0.0' + chai: ^4.2.0 + ethers: ^5.4.7 + hardhat: ^2.11.0 + hardhat-gas-reporter: ^1.0.8 + solidity-coverage: ^0.8.1 + ts-node: '>=8.0.0' + typechain: ^8.1.0 + typescript: '>=4.5.0' + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/providers': 5.7.1 + '@nomicfoundation/hardhat-chai-matchers': 1.0.3_u4bbm4ajgevldrm2ufutb7vine + '@nomicfoundation/hardhat-network-helpers': 1.0.6_hardhat@2.11.2 + '@nomiclabs/hardhat-ethers': 2.1.1_liuail6phkx7un26teqxcf6yx4 + '@nomiclabs/hardhat-etherscan': 3.1.0_hardhat@2.11.2 + '@typechain/ethers-v5': 10.1.0_ap4nli2xeolmikzlf5243gdnwy + '@typechain/hardhat': 6.1.3_hdc77mquumuyu4r5wip4q5ohha + '@types/chai': 4.3.3 + '@types/mocha': 9.1.1 + '@types/node': 18.7.13 + chai: 4.3.6 + ethers: 5.7.1 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + hardhat-gas-reporter: 1.0.9_hardhat@2.11.2 + solidity-coverage: 0.8.2_hardhat@2.11.2 + ts-node: 10.9.1_ieummqxttktzud32hpyrer46t4 + typechain: 8.1.0_typescript@4.8.4 + typescript: 4.8.4 + dev: true + + /@nomicfoundation/solidity-analyzer-darwin-arm64/0.0.3: + resolution: {integrity: sha512-W+bIiNiZmiy+MTYFZn3nwjyPUO6wfWJ0lnXx2zZrM8xExKObMrhCh50yy8pQING24mHfpPFCn89wEB/iG7vZDw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-darwin-x64/0.0.3: + resolution: {integrity: sha512-HuJd1K+2MgmFIYEpx46uzwEFjvzKAI765mmoMxy4K+Aqq1p+q7hHRlsFU2kx3NB8InwotkkIq3A5FLU1sI1WDw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-freebsd-x64/0.0.3: + resolution: {integrity: sha512-2cR8JNy23jZaO/vZrsAnWCsO73asU7ylrHIe0fEsXbZYqBP9sMr+/+xP3CELDHJxUbzBY8zqGvQt1ULpyrG+Kw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-linux-arm64-gnu/0.0.3: + resolution: {integrity: sha512-Eyv50EfYbFthoOb0I1568p+eqHGLwEUhYGOxcRNywtlTE9nj+c+MT1LA53HnxD9GsboH4YtOOmJOulrjG7KtbA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-linux-arm64-musl/0.0.3: + resolution: {integrity: sha512-V8grDqI+ivNrgwEt2HFdlwqV2/EQbYAdj3hbOvjrA8Qv+nq4h9jhQUxFpegYMDtpU8URJmNNlXgtfucSrAQwtQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-linux-x64-gnu/0.0.3: + resolution: {integrity: sha512-uRfVDlxtwT1vIy7MAExWAkRD4r9M79zMG7S09mCrWUn58DbLs7UFl+dZXBX0/8FTGYWHhOT/1Etw1ZpAf5DTrg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-linux-x64-musl/0.0.3: + resolution: {integrity: sha512-8HPwYdLbhcPpSwsE0yiU/aZkXV43vlXT2ycH+XlOjWOnLfH8C41z0njK8DHRtEFnp4OVN6E7E5lHBBKDZXCliA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-win32-arm64-msvc/0.0.3: + resolution: {integrity: sha512-5WWcT6ZNvfCuxjlpZOY7tdvOqT1kIQYlDF9Q42wMpZ5aTm4PvjdCmFDDmmTvyXEBJ4WTVmY5dWNWaxy8h/E28g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-win32-ia32-msvc/0.0.3: + resolution: {integrity: sha512-P/LWGZwWkyjSwkzq6skvS2wRc3gabzAbk6Akqs1/Iiuggql2CqdLBkcYWL5Xfv3haynhL+2jlNkak+v2BTZI4A==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer-win32-x64-msvc/0.0.3: + resolution: {integrity: sha512-4AcTtLZG1s/S5mYAIr/sdzywdNwJpOcdStGF3QMBzEt+cGn3MchMaS9b1gyhb2KKM2c39SmPF5fUuWq1oBSQZQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@nomicfoundation/solidity-analyzer/0.0.3: + resolution: {integrity: sha512-VFMiOQvsw7nx5bFmrmVp2Q9rhIjw2AFST4DYvWVVO9PMHPE23BY2+kyfrQ4J3xCMFC8fcBbGLt7l4q7m1SlTqg==} + engines: {node: '>= 12'} + optionalDependencies: + '@nomicfoundation/solidity-analyzer-darwin-arm64': 0.0.3 + '@nomicfoundation/solidity-analyzer-darwin-x64': 0.0.3 + '@nomicfoundation/solidity-analyzer-freebsd-x64': 0.0.3 + '@nomicfoundation/solidity-analyzer-linux-arm64-gnu': 0.0.3 + '@nomicfoundation/solidity-analyzer-linux-arm64-musl': 0.0.3 + '@nomicfoundation/solidity-analyzer-linux-x64-gnu': 0.0.3 + '@nomicfoundation/solidity-analyzer-linux-x64-musl': 0.0.3 + '@nomicfoundation/solidity-analyzer-win32-arm64-msvc': 0.0.3 + '@nomicfoundation/solidity-analyzer-win32-ia32-msvc': 0.0.3 + '@nomicfoundation/solidity-analyzer-win32-x64-msvc': 0.0.3 + dev: true + + /@nomiclabs/hardhat-ethers/2.1.1_liuail6phkx7un26teqxcf6yx4: + resolution: {integrity: sha512-Gg0IFkT/DW3vOpih4/kMjeZCLYqtfgECLeLXTs7ZDPzcK0cfoc5wKk4nq5n/izCUzdhidO/Utd6ptF9JrWwWVA==} + peerDependencies: + ethers: ^5.0.0 + hardhat: ^2.0.0 + dependencies: + ethers: 5.7.1 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + dev: true + + /@nomiclabs/hardhat-etherscan/3.1.0_hardhat@2.11.2: + resolution: {integrity: sha512-JroYgfN1AlYFkQTQ3nRwFi4o8NtZF7K/qFR2dxDUgHbCtIagkUseca9L4E/D2ScUm4XT40+8PbCdqZi+XmHyQA==} + peerDependencies: + hardhat: ^2.0.4 + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/address': 5.7.0 + cbor: 5.2.0 + chalk: 2.4.2 + debug: 4.3.4 + fs-extra: 7.0.1 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + lodash: 4.17.21 + semver: 6.3.0 + table: 6.8.0 + undici: 5.11.0 + transitivePeerDependencies: + - supports-color + dev: true + + /@openzeppelin/contracts/4.7.3: + resolution: {integrity: sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==} + dev: false + + /@scure/base/1.1.1: + resolution: {integrity: sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==} + dev: true + + /@scure/bip32/1.1.0: + resolution: {integrity: sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==} + dependencies: + '@noble/hashes': 1.1.2 + '@noble/secp256k1': 1.6.3 + '@scure/base': 1.1.1 + dev: true + + /@scure/bip39/1.1.0: + resolution: {integrity: sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==} + dependencies: + '@noble/hashes': 1.1.2 + '@scure/base': 1.1.1 + dev: true + + /@sentry/core/5.30.0: + resolution: {integrity: sha512-TmfrII8w1PQZSZgPpUESqjB+jC6MvZJZdLtE/0hZ+SrnKhW3x5WlYLvTXZpcWePYBku7rl2wn1RZu6uT0qCTeg==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + dev: true + + /@sentry/hub/5.30.0: + resolution: {integrity: sha512-2tYrGnzb1gKz2EkMDQcfLrDTvmGcQPuWxLnJKXJvYTQDGLlEvi2tWz1VIHjunmOvJrB5aIQLhm+dcMRwFZDCqQ==} + engines: {node: '>=6'} + dependencies: + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + dev: true + + /@sentry/minimal/5.30.0: + resolution: {integrity: sha512-BwWb/owZKtkDX+Sc4zCSTNcvZUq7YcH3uAVlmh/gtR9rmUvbzAA3ewLuB3myi4wWRAMEtny6+J/FN/x+2wn9Xw==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/types': 5.30.0 + tslib: 1.14.1 + dev: true + + /@sentry/node/5.30.0: + resolution: {integrity: sha512-Br5oyVBF0fZo6ZS9bxbJZG4ApAjRqAnqFFurMVJJdunNb80brh7a5Qva2kjhm+U6r9NJAB5OmDyPkA1Qnt+QVg==} + engines: {node: '>=6'} + dependencies: + '@sentry/core': 5.30.0 + '@sentry/hub': 5.30.0 + '@sentry/tracing': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + cookie: 0.4.2 + https-proxy-agent: 5.0.1 + lru_map: 0.3.3 + tslib: 1.14.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@sentry/tracing/5.30.0: + resolution: {integrity: sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==} + engines: {node: '>=6'} + dependencies: + '@sentry/hub': 5.30.0 + '@sentry/minimal': 5.30.0 + '@sentry/types': 5.30.0 + '@sentry/utils': 5.30.0 + tslib: 1.14.1 + dev: true + + /@sentry/types/5.30.0: + resolution: {integrity: sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==} + engines: {node: '>=6'} + dev: true + + /@sentry/utils/5.30.0: + resolution: {integrity: sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==} + engines: {node: '>=6'} + dependencies: + '@sentry/types': 5.30.0 + tslib: 1.14.1 + dev: true + + /@solidity-parser/parser/0.14.3: + resolution: {integrity: sha512-29g2SZ29HtsqA58pLCtopI1P/cPy5/UAzlcAXO6T/CNJimG6yA8kx4NaseMyJULiC+TEs02Y9/yeHzClqoA0hw==} + dependencies: + antlr4ts: 0.5.0-alpha.4 + dev: true + + /@tsconfig/node10/1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12/1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14/1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16/1.0.3: + resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==} + dev: true + + /@typechain/ethers-v5/10.1.0_ap4nli2xeolmikzlf5243gdnwy: + resolution: {integrity: sha512-3LIb+eUpV3mNCrjUKT5oqp8PBsZYSnVrkfk6pY/ZM0boRs2mKxjFZ7bktx42vfDye8PPz3NxtW4DL5NsNsFqlg==} + peerDependencies: + '@ethersproject/abi': ^5.0.0 + '@ethersproject/bytes': ^5.0.0 + '@ethersproject/providers': ^5.0.0 + ethers: ^5.1.3 + typechain: ^8.1.0 + typescript: '>=4.3.0' + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/providers': 5.7.1 + ethers: 5.7.1 + lodash: 4.17.21 + ts-essentials: 7.0.3_typescript@4.8.4 + typechain: 8.1.0_typescript@4.8.4 + typescript: 4.8.4 + dev: true + + /@typechain/hardhat/6.1.3_hdc77mquumuyu4r5wip4q5ohha: + resolution: {integrity: sha512-e1H9MVl286ma0HuD9CBL248+pbdA7lWF6+I7FYwzykIrjilKhvLUv0Q7LtcyZztzgbP2g4Tyg1UPE+xy+qR7cA==} + peerDependencies: + '@ethersproject/abi': ^5.4.7 + '@ethersproject/providers': ^5.4.7 + '@typechain/ethers-v5': ^10.1.0 + ethers: ^5.4.7 + hardhat: ^2.9.9 + typechain: ^8.1.0 + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/providers': 5.7.1 + '@typechain/ethers-v5': 10.1.0_ap4nli2xeolmikzlf5243gdnwy + ethers: 5.7.1 + fs-extra: 9.1.0 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + typechain: 8.1.0_typescript@4.8.4 + dev: true + + /@types/async-eventemitter/0.2.1: + resolution: {integrity: sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==} + dev: true + + /@types/bn.js/4.11.6: + resolution: {integrity: sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@types/bn.js/5.1.1: + resolution: {integrity: sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@types/chai-as-promised/7.1.5: + resolution: {integrity: sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==} + dependencies: + '@types/chai': 4.3.3 + dev: true + + /@types/chai/4.3.3: + resolution: {integrity: sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==} + dev: true + + /@types/concat-stream/1.6.1: + resolution: {integrity: sha512-eHE4cQPoj6ngxBZMvVf6Hw7Mh4jMW4U9lpGmS5GBPB9RYxlFg+CHaVN7ErNY4W9XfLIEn20b4VDYaIrbq0q4uA==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@types/form-data/0.0.33: + resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@types/glob/7.2.0: + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + dependencies: + '@types/minimatch': 5.1.2 + '@types/node': 18.7.13 + dev: true + + /@types/lru-cache/5.1.1: + resolution: {integrity: sha512-ssE3Vlrys7sdIzs5LOxCzTVMsU7i9oa/IaW92wF32JFb3CVczqOkru2xspuKczHEbG3nvmPY7IFqVmGGHdNbYw==} + dev: true + + /@types/minimatch/5.1.2: + resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} + dev: true + + /@types/mocha/9.1.1: + resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} + dev: true + + /@types/node/10.17.60: + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + dev: true + + /@types/node/18.7.13: + resolution: {integrity: sha512-46yIhxSe5xEaJZXWdIBP7GU4HDTG8/eo0qd9atdiL+lFpA03y8KS+lkTN834TWJj5767GbWv4n/P6efyTFt1Dw==} + dev: true + + /@types/node/8.10.66: + resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} + dev: true + + /@types/pbkdf2/3.1.0: + resolution: {integrity: sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@types/prettier/2.7.1: + resolution: {integrity: sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==} + dev: true + + /@types/qs/6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + dev: true + + /@types/secp256k1/4.0.3: + resolution: {integrity: sha512-Da66lEIFeIz9ltsdMZcpQvmrmmoqrfju8pm1BH8WbYjZSwUgCwXLb9C+9XYogwBITnbsSaMdVPb2ekf7TV+03w==} + dependencies: + '@types/node': 18.7.13 + dev: true + + /@ungap/promise-all-settled/1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: true + + /abbrev/1.0.9: + resolution: {integrity: sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==} + dev: true + + /abort-controller/3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: true + + /abstract-level/1.0.3: + resolution: {integrity: sha512-t6jv+xHy+VYwc4xqZMn2Pa9DjcdzvzZmQGRjTFc8spIbRGHgBrEKbPq+rYXc7CCo0lxgYvSgKVg9qZAhpVQSjA==} + engines: {node: '>=12'} + dependencies: + buffer: 6.0.3 + catering: 2.1.1 + is-buffer: 2.0.5 + level-supports: 4.0.1 + level-transcoder: 1.0.1 + module-error: 1.0.2 + queue-microtask: 1.2.3 + dev: true + + /acorn-walk/8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn/8.8.0: + resolution: {integrity: sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /address/1.2.1: + resolution: {integrity: sha512-B+6bi5D34+fDYENiH5qOlA0cV2rAGKuWZ9LeyUUehbXy8e0VS9e498yO0Jeeh+iM+6KbfudHTFjXw2MmJD4QRA==} + engines: {node: '>= 10.0.0'} + dev: true + + /adm-zip/0.4.16: + resolution: {integrity: sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==} + engines: {node: '>=0.3.0'} + dev: true + + /aes-js/3.0.0: + resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} + dev: true + + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /aggregate-error/3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + dev: true + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ajv/8.11.0: + resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==} + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js: 4.4.1 + dev: true + + /amdefine/1.0.1: + resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} + engines: {node: '>=0.4.2'} + dev: true + optional: true + + /ansi-colors/3.2.3: + resolution: {integrity: sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==} + engines: {node: '>=6'} + dev: true + + /ansi-colors/4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-colors/4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + dev: true + + /ansi-escapes/4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + dev: true + + /ansi-regex/3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + dev: true + + /ansi-regex/4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + dev: true + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles/3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /antlr4ts/0.5.0-alpha.4: + resolution: {integrity: sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==} + dev: true + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /arg/4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /argparse/1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-back/3.1.0: + resolution: {integrity: sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==} + engines: {node: '>=6'} + dev: true + + /array-back/4.0.2: + resolution: {integrity: sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==} + engines: {node: '>=8'} + dev: true + + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array-uniq/1.0.3: + resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} + engines: {node: '>=0.10.0'} + dev: true + + /array.prototype.reduce/1.0.4: + resolution: {integrity: sha512-WnM+AjG/DvLRLo4DDl+r+SvCzYtD2Jd9oeBYMcEaI7t3fFrHY9M53/wdLcTvmZNQ70IU6Htj0emFkZ5TS+lrdw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + es-array-method-boxes-properly: 1.0.0 + is-string: 1.0.7 + dev: true + + /asap/2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: true + + /asn1/0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /assert-plus/1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: true + + /assertion-error/1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: true + + /astral-regex/2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + dev: true + + /async-eventemitter/0.2.4: + resolution: {integrity: sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==} + dependencies: + async: 2.6.4 + dev: true + + /async/1.5.2: + resolution: {integrity: sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==} + dev: true + + /async/2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + dependencies: + lodash: 4.17.21 + dev: true + + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: true + + /at-least-node/1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + dev: true + + /aws-sign2/0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: true + + /aws4/1.11.0: + resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /base-x/3.0.9: + resolution: {integrity: sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /base64-js/1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: true + + /bcrypt-pbkdf/1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: true + + /bech32/1.1.4: + resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==} + dev: true + + /bigint-crypto-utils/3.1.7: + resolution: {integrity: sha512-zpCQpIE2Oy5WIQpjC9iYZf8Uh9QqoS51ZCooAcNvzv1AQ3VWdT52D0ksr1+/faeK8HVIej1bxXcP75YcqH3KPA==} + engines: {node: '>=10.4.0'} + dependencies: + bigint-mod-arith: 3.1.2 + dev: true + + /bigint-mod-arith/3.1.2: + resolution: {integrity: sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ==} + engines: {node: '>=10.4.0'} + dev: true + + /bignumber.js/9.1.0: + resolution: {integrity: sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A==} + dev: true + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /blakejs/1.2.1: + resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} + dev: true + + /bn.js/4.11.6: + resolution: {integrity: sha512-XWwnNNFCuuSQ0m3r3C4LE3EiORltHd9M05pq6FOlVeiophzRbMo50Sbz1ehl8K3Z+jw9+vmgnXefY1hz8X+2wA==} + dev: true + + /bn.js/4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: true + + /bn.js/5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /brorand/1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: true + + /browser-level/1.0.1: + resolution: {integrity: sha512-XECYKJ+Dbzw0lbydyQuJzwNXtOpbMSq737qxJN11sIRTErOMShvDpbzTlgju7orJKvx4epULolZAuJGLzCmWRQ==} + dependencies: + abstract-level: 1.0.3 + catering: 2.1.1 + module-error: 1.0.2 + run-parallel-limit: 1.1.0 + dev: true + + /browser-stdout/1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /browserify-aes/1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} + dependencies: + buffer-xor: 1.0.3 + cipher-base: 1.0.4 + create-hash: 1.2.0 + evp_bytestokey: 1.0.3 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /bs58/4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} + dependencies: + base-x: 3.0.9 + dev: true + + /bs58check/2.1.2: + resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} + dependencies: + bs58: 4.0.1 + create-hash: 1.2.0 + safe-buffer: 5.2.1 + dev: true + + /buffer-from/1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /buffer-xor/1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: true + + /buffer/6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: true + + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + + /bytes/3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: true + + /call-bind/1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.1.3 + dev: true + + /camelcase/5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + dev: true + + /camelcase/6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: true + + /catering/2.1.1: + resolution: {integrity: sha512-K7Qy8O9p76sL3/3m7/zLKbRkyOlSZAgzEaLhyj2mXS8PsCud2Eo4hAb8aLtZqHh0QGqLcb9dlJSu6lHRVENm1w==} + engines: {node: '>=6'} + dev: true + + /cbor/5.2.0: + resolution: {integrity: sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==} + engines: {node: '>=6.0.0'} + dependencies: + bignumber.js: 9.1.0 + nofilter: 1.0.4 + dev: true + + /chai-as-promised/7.1.1_chai@4.3.6: + resolution: {integrity: sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==} + peerDependencies: + chai: '>= 2.1.2 < 5' + dependencies: + chai: 4.3.6 + check-error: 1.0.2 + dev: true + + /chai/4.3.6: + resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} + engines: {node: '>=4'} + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 3.0.1 + get-func-name: 2.0.0 + loupe: 2.3.4 + pathval: 1.1.1 + type-detect: 4.0.8 + dev: true + + /chalk/2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /charenc/0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + dev: true + + /check-error/1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + dev: true + + /chokidar/3.3.0: + resolution: {integrity: sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.2.0 + optionalDependencies: + fsevents: 2.1.3 + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /ci-info/2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + dev: true + + /cipher-base/1.0.4: + resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /classic-level/1.2.0: + resolution: {integrity: sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==} + engines: {node: '>=12'} + requiresBuild: true + dependencies: + abstract-level: 1.0.3 + catering: 2.1.1 + module-error: 1.0.2 + napi-macros: 2.0.0 + node-gyp-build: 4.5.0 + dev: true + + /clean-stack/2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + dev: true + + /cli-table3/0.5.1: + resolution: {integrity: sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==} + engines: {node: '>=6'} + dependencies: + object-assign: 4.1.1 + string-width: 2.1.1 + optionalDependencies: + colors: 1.4.0 + dev: true + + /cliui/5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + dev: true + + /cliui/7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert/1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /colors/1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + dev: true + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: true + + /command-exists/1.2.9: + resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} + dev: true + + /command-line-args/5.2.1: + resolution: {integrity: sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + find-replace: 3.0.0 + lodash.camelcase: 4.3.0 + typical: 4.0.0 + dev: true + + /command-line-usage/6.1.3: + resolution: {integrity: sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + chalk: 2.4.2 + table-layout: 1.0.2 + typical: 5.2.0 + dev: true + + /commander/3.0.2: + resolution: {integrity: sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} + dev: true + + /concat-stream/1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.7 + typedarray: 0.0.6 + dev: true + + /cookie/0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: true + + /core-util-is/1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: true + + /core-util-is/1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + dev: true + + /crc-32/1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + dev: true + + /create-hash/1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} + dependencies: + cipher-base: 1.0.4 + inherits: 2.0.4 + md5.js: 1.3.5 + ripemd160: 2.0.2 + sha.js: 2.4.11 + dev: true + + /create-hmac/1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + dependencies: + cipher-base: 1.0.4 + create-hash: 1.2.0 + inherits: 2.0.4 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /create-require/1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /crypt/0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + dev: true + + /dashdash/1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: true + + /death/1.1.0: + resolution: {integrity: sha512-vsV6S4KVHvTGxbEcij7hkWRv0It+sGGWVOM67dQde/o5Xjnr+KmLjxWJii2uEObIrt1CcM9w0Yaovx+iOlIL+w==} + dev: true + + /debug/3.2.6_supports-color@6.0.0: + resolution: {integrity: sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==} + deprecated: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797) + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + supports-color: 6.0.0 + dev: true + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /debug/4.3.4_supports-color@8.1.1: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decamelize/1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + dev: true + + /decamelize/4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /deep-eql/3.0.1: + resolution: {integrity: sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==} + engines: {node: '>=0.12'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-eql/4.1.1: + resolution: {integrity: sha512-rc6HkZswtl+KMi/IODZ8k7C/P37clC2Rf1HYI11GqdbgvggIyHjsU5MdjlTlaP6eu24c0sR3mcW2SqsVZ1sXUw==} + engines: {node: '>=6'} + dependencies: + type-detect: 4.0.8 + dev: true + + /deep-extend/0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + dev: true + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /define-properties/1.1.4: + resolution: {integrity: sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==} + engines: {node: '>= 0.4'} + dependencies: + has-property-descriptors: 1.0.0 + object-keys: 1.1.1 + dev: true + + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: true + + /depd/2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: true + + /detect-port/1.5.1: + resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} + hasBin: true + dependencies: + address: 1.2.1 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /diff/3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + dev: true + + /diff/4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /difflib/0.2.4: + resolution: {integrity: sha512-9YVwmMb0wQHQNr5J9m6BSj6fk4pfGITGQOOs+D9Fl+INODWFOfvhIU1hNv6GgR1RBoC/9NJcwu77zShxV0kT7w==} + dependencies: + heap: 0.2.7 + dev: true + + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /ecc-jsbn/0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: true + + /elliptic/6.5.4: + resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==} + dependencies: + bn.js: 4.12.0 + brorand: 1.1.0 + hash.js: 1.1.7 + hmac-drbg: 1.0.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /emoji-regex/7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + dev: true + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /enquirer/2.3.6: + resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==} + engines: {node: '>=8.6'} + dependencies: + ansi-colors: 4.1.3 + dev: true + + /env-paths/2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + dev: true + + /es-abstract/1.20.4: + resolution: {integrity: sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + es-to-primitive: 1.2.1 + function-bind: 1.1.1 + function.prototype.name: 1.1.5 + get-intrinsic: 1.1.3 + get-symbol-description: 1.0.0 + has: 1.0.3 + has-property-descriptors: 1.0.0 + has-symbols: 1.0.3 + internal-slot: 1.0.3 + is-callable: 1.2.7 + is-negative-zero: 2.0.2 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.2 + is-string: 1.0.7 + is-weakref: 1.0.2 + object-inspect: 1.12.2 + object-keys: 1.1.1 + object.assign: 4.1.4 + regexp.prototype.flags: 1.4.3 + safe-regex-test: 1.0.0 + string.prototype.trimend: 1.0.5 + string.prototype.trimstart: 1.0.5 + unbox-primitive: 1.0.2 + dev: true + + /es-array-method-boxes-properly/1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + dev: true + + /es-to-primitive/1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp/1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + dev: true + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /escodegen/1.8.1: + resolution: {integrity: sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==} + engines: {node: '>=0.12.0'} + hasBin: true + dependencies: + esprima: 2.7.3 + estraverse: 1.9.3 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.2.0 + dev: true + + /esprima/2.7.3: + resolution: {integrity: sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==} + engines: {node: '>=0.10.0'} + hasBin: true + dev: true + + /esprima/4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /estraverse/1.9.3: + resolution: {integrity: sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==} + engines: {node: '>=0.10.0'} + dev: true + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eth-gas-reporter/0.2.25: + resolution: {integrity: sha512-1fRgyE4xUB8SoqLgN3eDfpDfwEfRxh2Sz1b7wzFbyQA+9TekMmvSjjoRu9SKcSVyK+vLkLIsVbJDsTWjw195OQ==} + peerDependencies: + '@codechecks/client': ^0.1.0 + peerDependenciesMeta: + '@codechecks/client': + optional: true + dependencies: + '@ethersproject/abi': 5.7.0 + '@solidity-parser/parser': 0.14.3 + cli-table3: 0.5.1 + colors: 1.4.0 + ethereum-cryptography: 1.1.2 + ethers: 4.0.49 + fs-readdir-recursive: 1.1.0 + lodash: 4.17.21 + markdown-table: 1.1.3 + mocha: 7.2.0 + req-cwd: 2.0.0 + request: 2.88.2 + request-promise-native: 1.0.9_request@2.88.2 + sha1: 1.1.1 + sync-request: 6.1.0 + dev: true + + /ethereum-bloom-filters/1.0.10: + resolution: {integrity: sha512-rxJ5OFN3RwjQxDcFP2Z5+Q9ho4eIdEmSc2ht0fCu8Se9nbXjZ7/031uXoUYJ87KHCOdVeiUuwSnoS7hmYAGVHA==} + dependencies: + js-sha3: 0.8.0 + dev: true + + /ethereum-cryptography/0.1.3: + resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} + dependencies: + '@types/pbkdf2': 3.1.0 + '@types/secp256k1': 4.0.3 + blakejs: 1.2.1 + browserify-aes: 1.2.0 + bs58check: 2.1.2 + create-hash: 1.2.0 + create-hmac: 1.1.7 + hash.js: 1.1.7 + keccak: 3.0.2 + pbkdf2: 3.1.2 + randombytes: 2.1.0 + safe-buffer: 5.2.1 + scrypt-js: 3.0.1 + secp256k1: 4.0.3 + setimmediate: 1.0.5 + dev: true + + /ethereum-cryptography/1.1.2: + resolution: {integrity: sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==} + dependencies: + '@noble/hashes': 1.1.2 + '@noble/secp256k1': 1.6.3 + '@scure/bip32': 1.1.0 + '@scure/bip39': 1.1.0 + dev: true + + /ethereumjs-abi/0.6.8: + resolution: {integrity: sha512-Tx0r/iXI6r+lRsdvkFDlut0N08jWMnKRZ6Gkq+Nmw75lZe4e6o3EkSnkaBP5NF6+m5PTGAr9JP43N3LyeoglsA==} + dependencies: + bn.js: 4.12.0 + ethereumjs-util: 6.2.1 + dev: true + + /ethereumjs-util/6.2.1: + resolution: {integrity: sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==} + dependencies: + '@types/bn.js': 4.11.6 + bn.js: 4.12.0 + create-hash: 1.2.0 + elliptic: 6.5.4 + ethereum-cryptography: 0.1.3 + ethjs-util: 0.1.6 + rlp: 2.2.7 + dev: true + + /ethereumjs-util/7.1.5: + resolution: {integrity: sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==} + engines: {node: '>=10.0.0'} + dependencies: + '@types/bn.js': 5.1.1 + bn.js: 5.2.1 + create-hash: 1.2.0 + ethereum-cryptography: 0.1.3 + rlp: 2.2.7 + dev: true + + /ethers/4.0.49: + resolution: {integrity: sha512-kPltTvWiyu+OktYy1IStSO16i2e7cS9D9OxZ81q2UUaiNPVrm/RTcbxamCXF9VUSKzJIdJV68EAIhTEVBalRWg==} + dependencies: + aes-js: 3.0.0 + bn.js: 4.12.0 + elliptic: 6.5.4 + hash.js: 1.1.3 + js-sha3: 0.5.7 + scrypt-js: 2.0.4 + setimmediate: 1.0.4 + uuid: 2.0.1 + xmlhttprequest: 1.8.0 + dev: true + + /ethers/5.7.1: + resolution: {integrity: sha512-5krze4dRLITX7FpU8J4WscXqADiKmyeNlylmmDLbS95DaZpBhDe2YSwRQwKXWNyXcox7a3gBgm/MkGXV1O1S/Q==} + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/abstract-provider': 5.7.0 + '@ethersproject/abstract-signer': 5.7.0 + '@ethersproject/address': 5.7.0 + '@ethersproject/base64': 5.7.0 + '@ethersproject/basex': 5.7.0 + '@ethersproject/bignumber': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/constants': 5.7.0 + '@ethersproject/contracts': 5.7.0 + '@ethersproject/hash': 5.7.0 + '@ethersproject/hdnode': 5.7.0 + '@ethersproject/json-wallets': 5.7.0 + '@ethersproject/keccak256': 5.7.0 + '@ethersproject/logger': 5.7.0 + '@ethersproject/networks': 5.7.1 + '@ethersproject/pbkdf2': 5.7.0 + '@ethersproject/properties': 5.7.0 + '@ethersproject/providers': 5.7.1 + '@ethersproject/random': 5.7.0 + '@ethersproject/rlp': 5.7.0 + '@ethersproject/sha2': 5.7.0 + '@ethersproject/signing-key': 5.7.0 + '@ethersproject/solidity': 5.7.0 + '@ethersproject/strings': 5.7.0 + '@ethersproject/transactions': 5.7.0 + '@ethersproject/units': 5.7.0 + '@ethersproject/wallet': 5.7.0 + '@ethersproject/web': 5.7.1 + '@ethersproject/wordlists': 5.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: true + + /ethjs-unit/0.1.6: + resolution: {integrity: sha1-xmWSHkduh7ziqdWIpv4EBbLEFpk=} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + bn.js: 4.11.6 + number-to-bn: 1.7.0 + dev: true + + /ethjs-util/0.1.6: + resolution: {integrity: sha512-CUnVOQq7gSpDHZVVrQW8ExxUETWrnrvXYvYz55wOU8Uj4VCgw56XC2B/fVqQN+f7gmrnRHSLVnFAwsCuNwji8w==} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + strip-hex-prefix: 1.0.0 + dev: true + + /event-target-shim/5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: true + + /evp_bytestokey/1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + dependencies: + md5.js: 1.3.5 + safe-buffer: 5.2.1 + dev: true + + /extend/3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: true + + /extsprintf/1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: true + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-replace/3.0.0: + resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} + engines: {node: '>=4.0.0'} + dependencies: + array-back: 3.1.0 + dev: true + + /find-up/2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + dependencies: + locate-path: 2.0.0 + dev: true + + /find-up/3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + dependencies: + locate-path: 3.0.0 + dev: true + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat/4.1.1: + resolution: {integrity: sha512-FmTtBsHskrU6FJ2VxCnsDb84wu9zhmO3cUX2kGFb5tuwhfXxGciiT0oRY+cck35QmG+NmGh5eLz6lLCpWTqwpA==} + hasBin: true + dependencies: + is-buffer: 2.0.5 + dev: true + + /flat/5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /follow-redirects/1.15.2_debug@4.3.4: + resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.4 + dev: true + + /forever-agent/0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: true + + /form-data/2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /form-data/2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: true + + /fp-ts/1.19.3: + resolution: {integrity: sha512-H5KQDspykdHuztLTg+ajGN0Z2qUjcEf3Ybxc6hLt0k7/zPkn29XnKnxlBPyW2XIddWrGaJBzBl4VLYOtk39yZg==} + dev: true + + /fs-extra/0.30.0: + resolution: {integrity: sha512-UvSPKyhMn6LEd/WpUaV9C9t3zATuqoqfWc3QdPhPLb58prN9tqYPlPWi8Krxi44loBoUzlobqZ3+8tGpxxSzwA==} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 2.4.0 + klaw: 1.3.1 + path-is-absolute: 1.0.1 + rimraf: 2.7.1 + dev: true + + /fs-extra/7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra/8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.10 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + + /fs-extra/9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.10 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: true + + /fs-readdir-recursive/1.1.0: + resolution: {integrity: sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==} + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.1.3: + resolution: {integrity: sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + deprecated: '"Please update to latest v2.3 or v2.2"' + requiresBuild: true + dev: true + optional: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /function.prototype.name/1.1.5: + resolution: {integrity: sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + functions-have-names: 1.2.3 + dev: true + + /functional-red-black-tree/1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + dev: true + + /functions-have-names/1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + dev: true + + /get-caller-file/2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /get-func-name/2.0.0: + resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + dev: true + + /get-intrinsic/1.1.3: + resolution: {integrity: sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: true + + /get-port/3.2.0: + resolution: {integrity: sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg==} + engines: {node: '>=4'} + dev: true + + /get-symbol-description/1.0.0: + resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + dev: true + + /getpass/0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: true + + /ghost-testrpc/0.0.2: + resolution: {integrity: sha512-i08dAEgJ2g8z5buJIrCTduwPIhih3DP+hOCTyyryikfV8T0bNvHnGXO67i0DD1H4GBDETTclPy9njZbfluQYrQ==} + hasBin: true + dependencies: + chalk: 2.4.2 + node-emoji: 1.11.0 + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/5.0.15: + resolution: {integrity: sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==} + dependencies: + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/7.1.3: + resolution: {integrity: sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/7.1.7: + resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /global-modules/2.0.0: + resolution: {integrity: sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==} + engines: {node: '>=6'} + dependencies: + global-prefix: 3.0.0 + dev: true + + /global-prefix/3.0.0: + resolution: {integrity: sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==} + engines: {node: '>=6'} + dependencies: + ini: 1.3.8 + kind-of: 6.0.3 + which: 1.3.1 + dev: true + + /globby/10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + glob: 7.2.3 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /growl/1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + dev: true + + /handlebars/4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.6 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.17.3 + dev: true + + /har-schema/2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: true + + /har-validator/5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: true + + /hardhat-gas-reporter/1.0.9_hardhat@2.11.2: + resolution: {integrity: sha512-INN26G3EW43adGKBNzYWOlI3+rlLnasXTwW79YNnUhXPDa+yHESgt639dJEs37gCjhkbNKcRRJnomXEuMFBXJg==} + peerDependencies: + hardhat: ^2.0.2 + dependencies: + array-uniq: 1.0.3 + eth-gas-reporter: 0.2.25 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + sha1: 1.1.1 + transitivePeerDependencies: + - '@codechecks/client' + dev: true + + /hardhat/2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka: + resolution: {integrity: sha512-BdsXC1CFJQDJKmAgCwpmGhFuVU6dcqlgMgT0Kg/xmFAFVugkpYu6NRmh4AaJ3Fah0/BR9DOR4XgQGIbg4eon/Q==} + engines: {node: ^14.0.0 || ^16.0.0 || ^18.0.0} + hasBin: true + peerDependencies: + ts-node: '*' + typescript: '*' + peerDependenciesMeta: + ts-node: + optional: true + typescript: + optional: true + dependencies: + '@ethersproject/abi': 5.7.0 + '@metamask/eth-sig-util': 4.0.1 + '@nomicfoundation/ethereumjs-block': 4.0.0 + '@nomicfoundation/ethereumjs-blockchain': 6.0.0 + '@nomicfoundation/ethereumjs-common': 3.0.0 + '@nomicfoundation/ethereumjs-evm': 1.0.0 + '@nomicfoundation/ethereumjs-rlp': 4.0.0 + '@nomicfoundation/ethereumjs-statemanager': 1.0.0 + '@nomicfoundation/ethereumjs-trie': 5.0.0 + '@nomicfoundation/ethereumjs-tx': 4.0.0 + '@nomicfoundation/ethereumjs-util': 8.0.0 + '@nomicfoundation/ethereumjs-vm': 6.0.0 + '@nomicfoundation/solidity-analyzer': 0.0.3 + '@sentry/node': 5.30.0 + '@types/bn.js': 5.1.1 + '@types/lru-cache': 5.1.1 + abort-controller: 3.0.0 + adm-zip: 0.4.16 + aggregate-error: 3.1.0 + ansi-escapes: 4.3.2 + chalk: 2.4.2 + chokidar: 3.5.3 + ci-info: 2.0.0 + debug: 4.3.4 + enquirer: 2.3.6 + env-paths: 2.2.1 + ethereum-cryptography: 1.1.2 + ethereumjs-abi: 0.6.8 + find-up: 2.1.0 + fp-ts: 1.19.3 + fs-extra: 7.0.1 + glob: 7.2.0 + immutable: 4.1.0 + io-ts: 1.10.4 + keccak: 3.0.2 + lodash: 4.17.21 + mnemonist: 0.38.5 + mocha: 10.0.0 + p-map: 4.0.0 + qs: 6.11.0 + raw-body: 2.5.1 + resolve: 1.17.0 + semver: 6.3.0 + solc: 0.7.3_debug@4.3.4 + source-map-support: 0.5.21 + stacktrace-parser: 0.1.10 + ts-node: 10.9.1_ieummqxttktzud32hpyrer46t4 + tsort: 0.0.1 + typescript: 4.8.4 + undici: 5.11.0 + uuid: 8.3.2 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /has-bigints/1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag/1.0.0: + resolution: {integrity: sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==} + engines: {node: '>=0.10.0'} + dev: true + + /has-flag/3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has-property-descriptors/1.0.0: + resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==} + dependencies: + get-intrinsic: 1.1.3 + dev: true + + /has-symbols/1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: true + + /has-tostringtag/1.0.0: + resolution: {integrity: sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /hash-base/3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} + dependencies: + inherits: 2.0.4 + readable-stream: 3.6.0 + safe-buffer: 5.2.1 + dev: true + + /hash.js/1.1.3: + resolution: {integrity: sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /hash.js/1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} + dependencies: + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + dev: true + + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /heap/0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + dev: true + + /hmac-drbg/1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} + dependencies: + hash.js: 1.1.7 + minimalistic-assert: 1.0.1 + minimalistic-crypto-utils: 1.0.1 + dev: true + + /http-basic/8.1.3: + resolution: {integrity: sha512-/EcDMwJZh3mABI2NhGfHOGOeOZITqfkEO4p/xK+l3NpyncIHUQBoMvCSF/b5GqvKtySC2srL/GGG3+EtlqlmCw==} + engines: {node: '>=6.0.0'} + dependencies: + caseless: 0.12.0 + concat-stream: 1.6.2 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + dev: true + + /http-errors/2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: true + + /http-response-object/3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + dependencies: + '@types/node': 10.17.60 + dev: true + + /http-signature/1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.17.0 + dev: true + + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /iconv-lite/0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ieee754/1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: true + + /ignore/5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: true + + /immutable/4.1.0: + resolution: {integrity: sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==} + dev: true + + /indent-string/4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /ini/1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + dev: true + + /internal-slot/1.0.3: + resolution: {integrity: sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.1.3 + has: 1.0.3 + side-channel: 1.0.4 + dev: true + + /interpret/1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + dev: true + + /io-ts/1.10.4: + resolution: {integrity: sha512-b23PteSnYXSONJ6JQXRAlvJhuw8KOtkqa87W4wDtvMrud/DTJd5X+NpOOI+O/zZwVq6v0VLAaJ+1EDViKEuN9g==} + dependencies: + fp-ts: 1.19.3 + dev: true + + /is-bigint/1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-boolean-object/1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-buffer/2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + dev: true + + /is-callable/1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module/2.10.0: + resolution: {integrity: sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==} + dependencies: + has: 1.0.3 + dev: true + + /is-date-object/1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point/2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + dev: true + + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-hex-prefixed/1.0.0: + resolution: {integrity: sha1-fY035q135dEnFIkTxXPggtd39VQ=} + engines: {node: '>=6.5.0', npm: '>=3'} + dev: true + + /is-negative-zero/2.0.2: + resolution: {integrity: sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object/1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj/2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-regex/1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + has-tostringtag: 1.0.0 + dev: true + + /is-shared-array-buffer/1.0.2: + resolution: {integrity: sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==} + dependencies: + call-bind: 1.0.2 + dev: true + + /is-string/1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.0 + dev: true + + /is-symbol/1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typedarray/1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: true + + /is-unicode-supported/0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /is-weakref/1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.2 + dev: true + + /isarray/1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + dev: true + + /isexe/2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /isstream/0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: true + + /js-sha3/0.5.7: + resolution: {integrity: sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g==} + dev: true + + /js-sha3/0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + dev: true + + /js-yaml/3.13.1: + resolution: {integrity: sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml/3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + dev: true + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsbn/0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: true + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-schema-traverse/1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + dev: true + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: true + + /json-stringify-safe/5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: true + + /jsonfile/2.4.0: + resolution: {integrity: sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==} + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsonfile/4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsonfile/6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.0 + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /jsonschema/1.4.1: + resolution: {integrity: sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==} + dev: true + + /jsprim/1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: true + + /keccak/3.0.2: + resolution: {integrity: sha512-PyKKjkH53wDMLGrvmRGSNWgmSxZOUqbnXwKL9tmgbFYA1iAYqW21kfR7mZXV0MlESiefxQQE9X9fTa3X+2MPDQ==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + node-addon-api: 2.0.2 + node-gyp-build: 4.5.0 + readable-stream: 3.6.0 + dev: true + + /kind-of/6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + dev: true + + /klaw/1.3.1: + resolution: {integrity: sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==} + optionalDependencies: + graceful-fs: 4.2.10 + dev: true + + /level-supports/4.0.1: + resolution: {integrity: sha512-PbXpve8rKeNcZ9C1mUicC9auIYFyGpkV9/i6g76tLgANwWhtG2v7I4xNBUlkn3lE2/dZF3Pi0ygYGtLc4RXXdA==} + engines: {node: '>=12'} + dev: true + + /level-transcoder/1.0.1: + resolution: {integrity: sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==} + engines: {node: '>=12'} + dependencies: + buffer: 6.0.3 + module-error: 1.0.2 + dev: true + + /level/8.0.0: + resolution: {integrity: sha512-ypf0jjAk2BWI33yzEaaotpq7fkOPALKAgDBxggO6Q9HGX2MRXn0wbP1Jn/tJv1gtL867+YOjOB49WaUF3UoJNQ==} + engines: {node: '>=12'} + dependencies: + browser-level: 1.0.1 + classic-level: 1.2.0 + dev: true + + /levn/0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + dev: true + + /locate-path/2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + dependencies: + p-locate: 2.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path/3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + dev: true + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.camelcase/4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + dev: true + + /lodash.truncate/4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + + /log-symbols/3.0.0: + resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} + engines: {node: '>=8'} + dependencies: + chalk: 2.4.2 + dev: true + + /log-symbols/4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loupe/2.3.4: + resolution: {integrity: sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==} + dependencies: + get-func-name: 2.0.0 + dev: true + + /lru-cache/5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + dev: true + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /lru_map/0.3.3: + resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} + dev: true + + /make-error/1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /markdown-table/1.1.3: + resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} + dev: true + + /mcl-wasm/0.7.9: + resolution: {integrity: sha512-iJIUcQWA88IJB/5L15GnJVnSQJmf/YaxxV6zRavv83HILHaJQb6y0iFyDMdDO0gN8X37tdxmAOrH/P8B6RB8sQ==} + engines: {node: '>=8.9.0'} + dev: true + + /md5.js/1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /memory-level/1.0.0: + resolution: {integrity: sha512-UXzwewuWeHBz5krr7EvehKcmLFNoXxGcvuYhC41tRnkrTbJohtS7kVn9akmgirtRygg+f7Yjsfi8Uu5SGSQ4Og==} + engines: {node: '>=12'} + dependencies: + abstract-level: 1.0.3 + functional-red-black-tree: 1.0.1 + module-error: 1.0.2 + dev: true + + /memorystream/0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: true + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: true + + /minimalistic-assert/1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: true + + /minimalistic-crypto-utils/1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: true + + /minimatch/3.0.4: + resolution: {integrity: sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist/1.2.6: + resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + dev: true + + /mkdirp/0.5.5: + resolution: {integrity: sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==} + hasBin: true + dependencies: + minimist: 1.2.6 + dev: true + + /mkdirp/0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.6 + dev: true + + /mkdirp/1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /mnemonist/0.38.5: + resolution: {integrity: sha512-bZTFT5rrPKtPJxj8KSV0WkPyNxl72vQepqqVUAW2ARUpUSF2qXMB6jZj7hW5/k7C1rtpzqbD/IIbJwLXUjCHeg==} + dependencies: + obliterator: 2.0.4 + dev: true + + /mocha/10.0.0: + resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4_supports-color@8.1.1 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /mocha/7.1.2: + resolution: {integrity: sha512-o96kdRKMKI3E8U0bjnfqW4QMk12MwZ4mhdBTf+B5a1q9+aq2HRnj+3ZdJu0B/ZhJeK78MgYuv6L8d/rA5AeBJA==} + engines: {node: '>= 8.10.0'} + hasBin: true + dependencies: + ansi-colors: 3.2.3 + browser-stdout: 1.3.1 + chokidar: 3.3.0 + debug: 3.2.6_supports-color@6.0.0 + diff: 3.5.0 + escape-string-regexp: 1.0.5 + find-up: 3.0.0 + glob: 7.1.3 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 3.13.1 + log-symbols: 3.0.0 + minimatch: 3.0.4 + mkdirp: 0.5.5 + ms: 2.1.1 + node-environment-flags: 1.0.6 + object.assign: 4.1.0 + strip-json-comments: 2.0.1 + supports-color: 6.0.0 + which: 1.3.1 + wide-align: 1.1.3 + yargs: 13.3.2 + yargs-parser: 13.1.2 + yargs-unparser: 1.6.0 + dev: true + + /mocha/7.2.0: + resolution: {integrity: sha512-O9CIypScywTVpNaRrCAgoUnJgozpIofjKUYmJhiCIJMiuYnLI6otcb1/kpW9/n/tJODHGZ7i8aLQoDVsMtOKQQ==} + engines: {node: '>= 8.10.0'} + hasBin: true + dependencies: + ansi-colors: 3.2.3 + browser-stdout: 1.3.1 + chokidar: 3.3.0 + debug: 3.2.6_supports-color@6.0.0 + diff: 3.5.0 + escape-string-regexp: 1.0.5 + find-up: 3.0.0 + glob: 7.1.3 + growl: 1.10.5 + he: 1.2.0 + js-yaml: 3.13.1 + log-symbols: 3.0.0 + minimatch: 3.0.4 + mkdirp: 0.5.5 + ms: 2.1.1 + node-environment-flags: 1.0.6 + object.assign: 4.1.0 + strip-json-comments: 2.0.1 + supports-color: 6.0.0 + which: 1.3.1 + wide-align: 1.1.3 + yargs: 13.3.2 + yargs-parser: 13.1.2 + yargs-unparser: 1.6.0 + dev: true + + /module-error/1.0.2: + resolution: {integrity: sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==} + engines: {node: '>=10'} + dev: true + + /ms/2.1.1: + resolution: {integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==} + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid/3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /napi-macros/2.0.0: + resolution: {integrity: sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==} + dev: true + + /neo-async/2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: true + + /node-addon-api/2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + dev: true + + /node-emoji/1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + dependencies: + lodash: 4.17.21 + dev: true + + /node-environment-flags/1.0.6: + resolution: {integrity: sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==} + dependencies: + object.getownpropertydescriptors: 2.1.4 + semver: 5.7.1 + dev: true + + /node-gyp-build/4.5.0: + resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==} + hasBin: true + dev: true + + /nofilter/1.0.4: + resolution: {integrity: sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==} + engines: {node: '>=8'} + dev: true + + /nopt/3.0.6: + resolution: {integrity: sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==} + hasBin: true + dependencies: + abbrev: 1.0.9 + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /number-to-bn/1.7.0: + resolution: {integrity: sha1-uzYjWS9+X54AMLGXe9QaDFP+HqA=} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + bn.js: 4.11.6 + strip-hex-prefix: 1.0.0 + dev: true + + /oauth-sign/0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: true + + /object-assign/4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: true + + /object-inspect/1.12.2: + resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} + dev: true + + /object-keys/1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + dev: true + + /object.assign/4.1.0: + resolution: {integrity: sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.1.4 + function-bind: 1.1.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.assign/4.1.4: + resolution: {integrity: sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.getownpropertydescriptors/2.1.4: + resolution: {integrity: sha512-sccv3L/pMModT6dJAYF3fzGMVcb38ysQ0tEE6ixv2yXJDtEIPph268OlAdJj5/qZMZDq2g/jqvwppt36uS/uQQ==} + engines: {node: '>= 0.8'} + dependencies: + array.prototype.reduce: 1.0.4 + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /obliterator/2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator/0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.3 + dev: true + + /ordinal/1.0.3: + resolution: {integrity: sha512-cMddMgb2QElm8G7vdaa02jhUNbTSrhsgAGUz1OokD83uJTwSUn+nKoNoKVVaRa08yF6sgfO7Maou1+bgLd9rdQ==} + dev: true + + /os-tmpdir/1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + dev: true + + /p-limit/1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + dependencies: + p-try: 1.0.0 + dev: true + + /p-limit/2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + dev: true + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate/2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + dependencies: + p-limit: 1.3.0 + dev: true + + /p-locate/3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + dependencies: + p-limit: 2.3.0 + dev: true + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-map/4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + dependencies: + aggregate-error: 3.1.0 + dev: true + + /p-try/1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + dev: true + + /p-try/2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + dev: true + + /parse-cache-control/1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + dev: true + + /path-exists/3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + dev: true + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /pathval/1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: true + + /pbkdf2/3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} + dependencies: + create-hash: 1.2.0 + create-hmac: 1.1.7 + ripemd160: 2.0.2 + safe-buffer: 5.2.1 + sha.js: 2.4.11 + dev: true + + /performance-now/2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /pify/4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: true + + /prelude-ls/1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier/2.7.1: + resolution: {integrity: sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /process-nextick-args/2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + dev: true + + /promise/8.2.0: + resolution: {integrity: sha512-+CMAlLHqwRYwBMXKCP+o8ns7DN+xHDUiI+0nArsiJ9y+kJVPLFxEaSw6Ha9s9H0tftxg2Yzl25wqj9G7m5wLZg==} + dependencies: + asap: 2.0.6 + dev: true + + /psl/1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode/2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: true + + /qs/6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: true + + /qs/6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /raw-body/2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: true + + /readable-stream/2.3.7: + resolution: {integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: true + + /readable-stream/3.6.0: + resolution: {integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + dev: true + + /readdirp/3.2.0: + resolution: {integrity: sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==} + engines: {node: '>= 8'} + dependencies: + picomatch: 2.3.1 + dev: true + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /rechoir/0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + dependencies: + resolve: 1.22.1 + dev: true + + /recursive-readdir/2.2.2: + resolution: {integrity: sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==} + engines: {node: '>=0.10.0'} + dependencies: + minimatch: 3.0.4 + dev: true + + /reduce-flatten/2.0.0: + resolution: {integrity: sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==} + engines: {node: '>=6'} + dev: true + + /regexp.prototype.flags/1.4.3: + resolution: {integrity: sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + functions-have-names: 1.2.3 + dev: true + + /req-cwd/2.0.0: + resolution: {integrity: sha512-ueoIoLo1OfB6b05COxAA9UpeoscNpYyM+BqYlA7H6LVF4hKGPXQQSSaD2YmvDVJMkk4UDpAHIeU1zG53IqjvlQ==} + engines: {node: '>=4'} + dependencies: + req-from: 2.0.0 + dev: true + + /req-from/2.0.0: + resolution: {integrity: sha512-LzTfEVDVQHBRfjOUMgNBA+V6DWsSnoeKzf42J7l0xa/B4jyPOuuF5MlNSmomLNGemWTnV2TIdjSSLnEn95fOQA==} + engines: {node: '>=4'} + dependencies: + resolve-from: 3.0.0 + dev: true + + /request-promise-core/1.1.4_request@2.88.2: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + dependencies: + lodash: 4.17.21 + request: 2.88.2 + dev: true + + /request-promise-native/1.0.9_request@2.88.2: + resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==} + engines: {node: '>=0.12.0'} + deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + dependencies: + request: 2.88.2 + request-promise-core: 1.1.4_request@2.88.2 + stealthy-require: 1.1.1 + tough-cookie: 2.5.0 + dev: true + + /request/2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.11.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: true + + /require-directory/2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /require-from-string/2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + dev: true + + /require-main-filename/2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + dev: true + + /resolve-from/3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + dev: true + + /resolve/1.1.7: + resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} + dev: true + + /resolve/1.17.0: + resolution: {integrity: sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==} + dependencies: + path-parse: 1.0.7 + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.10.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: true + + /ripemd160/2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} + dependencies: + hash-base: 3.1.0 + inherits: 2.0.4 + dev: true + + /rlp/2.2.7: + resolution: {integrity: sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==} + hasBin: true + dependencies: + bn.js: 5.2.1 + dev: true + + /run-parallel-limit/1.1.0: + resolution: {integrity: sha512-jJA7irRNM91jaKc3Hcl1npHsFLOXOoTkPCUL1JEa1R82O2miplXXRaGdjW/KM/98YQWDhJLiSs793CnXfblJUw==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rustbn.js/0.2.0: + resolution: {integrity: sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==} + dev: true + + /safe-buffer/5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /safe-regex-test/1.0.0: + resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + is-regex: 1.1.4 + dev: true + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sc-istanbul/0.4.6: + resolution: {integrity: sha512-qJFF/8tW/zJsbyfh/iT/ZM5QNHE3CXxtLJbZsL+CzdJLBsPD7SedJZoUA4d8iAcN2IoMp/Dx80shOOd2x96X/g==} + hasBin: true + dependencies: + abbrev: 1.0.9 + async: 1.5.2 + escodegen: 1.8.1 + esprima: 2.7.3 + glob: 5.0.15 + handlebars: 4.7.7 + js-yaml: 3.14.1 + mkdirp: 0.5.6 + nopt: 3.0.6 + once: 1.4.0 + resolve: 1.1.7 + supports-color: 3.2.3 + which: 1.3.1 + wordwrap: 1.0.0 + dev: true + + /scrypt-js/2.0.4: + resolution: {integrity: sha512-4KsaGcPnuhtCZQCxFxN3GVYIhKFPTdLd8PLC552XwbMndtD0cjRFAhDuuydXQ0h08ZfPgzqe6EKHozpuH74iDw==} + dev: true + + /scrypt-js/3.0.1: + resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} + dev: true + + /secp256k1/4.0.3: + resolution: {integrity: sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==} + engines: {node: '>=10.0.0'} + requiresBuild: true + dependencies: + elliptic: 6.5.4 + node-addon-api: 2.0.2 + node-gyp-build: 4.5.0 + dev: true + + /semver/5.7.1: + resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} + hasBin: true + dev: true + + /semver/6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: true + + /semver/7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /serialize-javascript/6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /set-blocking/2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: true + + /setimmediate/1.0.4: + resolution: {integrity: sha512-/TjEmXQVEzdod/FFskf3o7oOAsGhHf2j1dZqRFbDzq4F3mvvxflIIi4Hd3bLQE9y/CpwqfSQam5JakI/mi3Pog==} + dev: true + + /setimmediate/1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: true + + /setprototypeof/1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: true + + /sha.js/2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true + dependencies: + inherits: 2.0.4 + safe-buffer: 5.2.1 + dev: true + + /sha1/1.1.1: + resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==} + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + dev: true + + /shelljs/0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + dev: true + + /side-channel/1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.1.3 + object-inspect: 1.12.2 + dev: true + + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /slice-ansi/4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + dev: true + + /solc/0.7.3_debug@4.3.4: + resolution: {integrity: sha512-GAsWNAjGzIDg7VxzP6mPjdurby3IkGCjQcM8GFYZT6RyaoUZKmMU6Y7YwG+tFGhv7dwZ8rmR4iwFDrrD99JwqA==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + command-exists: 1.2.9 + commander: 3.0.2 + follow-redirects: 1.15.2_debug@4.3.4 + fs-extra: 0.30.0 + js-sha3: 0.8.0 + memorystream: 0.3.1 + require-from-string: 2.0.2 + semver: 5.7.1 + tmp: 0.0.33 + transitivePeerDependencies: + - debug + dev: true + + /solidity-coverage/0.8.2_hardhat@2.11.2: + resolution: {integrity: sha512-cv2bWb7lOXPE9/SSleDO6czkFiMHgP4NXPj+iW9W7iEKLBk7Cj0AGBiNmGX3V1totl9wjPrT0gHmABZKZt65rQ==} + hasBin: true + peerDependencies: + hardhat: ^2.11.0 + dependencies: + '@ethersproject/abi': 5.7.0 + '@solidity-parser/parser': 0.14.3 + chalk: 2.4.2 + death: 1.1.0 + detect-port: 1.5.1 + difflib: 0.2.4 + fs-extra: 8.1.0 + ghost-testrpc: 0.0.2 + global-modules: 2.0.0 + globby: 10.0.2 + hardhat: 2.11.2_mwhvu7sfp6vq5ryuwb6hlbjfka + jsonschema: 1.4.1 + lodash: 4.17.21 + mocha: 7.1.2 + node-emoji: 1.11.0 + pify: 4.0.1 + recursive-readdir: 2.2.2 + sc-istanbul: 0.4.6 + semver: 7.3.8 + shelljs: 0.8.5 + web3-utils: 1.8.0 + transitivePeerDependencies: + - supports-color + dev: true + + /source-map-support/0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map/0.2.0: + resolution: {integrity: sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==} + engines: {node: '>=0.8.0'} + requiresBuild: true + dependencies: + amdefine: 1.0.1 + dev: true + optional: true + + /source-map/0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /sprintf-js/1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + + /sshpk/1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: true + + /stacktrace-parser/0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + dependencies: + type-fest: 0.7.1 + dev: true + + /statuses/2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: true + + /stealthy-require/1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + dev: true + + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true + + /string-format/2.0.0: + resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} + dev: true + + /string-width/2.1.1: + resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==} + engines: {node: '>=4'} + dependencies: + is-fullwidth-code-point: 2.0.0 + strip-ansi: 4.0.0 + dev: true + + /string-width/3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + dev: true + + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /string.prototype.trimend/1.0.5: + resolution: {integrity: sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /string.prototype.trimstart/1.0.5: + resolution: {integrity: sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==} + dependencies: + call-bind: 1.0.2 + define-properties: 1.1.4 + es-abstract: 1.20.4 + dev: true + + /string_decoder/1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /string_decoder/1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /strip-ansi/4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + dependencies: + ansi-regex: 3.0.1 + dev: true + + /strip-ansi/5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + dependencies: + ansi-regex: 4.1.1 + dev: true + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-hex-prefix/1.0.0: + resolution: {integrity: sha1-DF8VX+8RUTczd96du1iNoFUA428=} + engines: {node: '>=6.5.0', npm: '>=3'} + dependencies: + is-hex-prefixed: 1.0.0 + dev: true + + /strip-json-comments/2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color/3.2.3: + resolution: {integrity: sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==} + engines: {node: '>=0.8.0'} + dependencies: + has-flag: 1.0.0 + dev: true + + /supports-color/5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color/6.0.0: + resolution: {integrity: sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==} + engines: {node: '>=6'} + dependencies: + has-flag: 3.0.0 + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color/8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /sync-request/6.1.0: + resolution: {integrity: sha512-8fjNkrNlNCrVc/av+Jn+xxqfCjYaBoHqCsDz6mt030UMxJGr+GSfCV1dQt2gRtlL63+VPidwDVLr7V2OcTSdRw==} + engines: {node: '>=8.0.0'} + dependencies: + http-response-object: 3.0.2 + sync-rpc: 1.3.6 + then-request: 6.0.2 + dev: true + + /sync-rpc/1.3.6: + resolution: {integrity: sha512-J8jTXuZzRlvU7HemDgHi3pGnh/rkoqR/OZSjhTyyZrEkkYQbk7Z33AXp37mkPfPpfdOuj7Ex3H/TJM1z48uPQw==} + dependencies: + get-port: 3.2.0 + dev: true + + /table-layout/1.0.2: + resolution: {integrity: sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==} + engines: {node: '>=8.0.0'} + dependencies: + array-back: 4.0.2 + deep-extend: 0.6.0 + typical: 5.2.0 + wordwrapjs: 4.0.1 + dev: true + + /table/6.8.0: + resolution: {integrity: sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA==} + engines: {node: '>=10.0.0'} + dependencies: + ajv: 8.11.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /then-request/6.0.2: + resolution: {integrity: sha512-3ZBiG7JvP3wbDzA9iNY5zJQcHL4jn/0BWtXIkagfz7QgOL/LqjCEOBQuJNZfu0XYnv5JhKh+cDxCPM4ILrqruA==} + engines: {node: '>=6.0.0'} + dependencies: + '@types/concat-stream': 1.6.1 + '@types/form-data': 0.0.33 + '@types/node': 8.10.66 + '@types/qs': 6.9.7 + caseless: 0.12.0 + concat-stream: 1.6.2 + form-data: 2.5.1 + http-basic: 8.1.3 + http-response-object: 3.0.2 + promise: 8.2.0 + qs: 6.11.0 + dev: true + + /tmp/0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + dependencies: + os-tmpdir: 1.0.2 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier/1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: true + + /tough-cookie/2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.9.0 + punycode: 2.1.1 + dev: true + + /ts-command-line-args/2.3.1: + resolution: {integrity: sha512-FR3y7pLl/fuUNSmnPhfLArGqRrpojQgIEEOVzYx9DhTmfIN7C9RWSfpkJEF4J+Gk7aVx5pak8I7vWZsaN4N84g==} + hasBin: true + dependencies: + chalk: 4.1.2 + command-line-args: 5.2.1 + command-line-usage: 6.1.3 + string-format: 2.0.0 + dev: true + + /ts-essentials/7.0.3_typescript@4.8.4: + resolution: {integrity: sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==} + peerDependencies: + typescript: '>=3.7.0' + dependencies: + typescript: 4.8.4 + dev: true + + /ts-node/10.9.1_ieummqxttktzud32hpyrer46t4: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.3 + '@types/node': 18.7.13 + acorn: 8.8.0 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.8.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tsort/0.0.1: + resolution: {integrity: sha1-4igPXoF/i/QnVlf9D5rr1E9aJ4Y=} + dev: true + + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /tweetnacl-util/0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + dev: true + + /tweetnacl/0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: true + + /tweetnacl/1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + dev: true + + /type-check/0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.1.2 + dev: true + + /type-detect/4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + dev: true + + /type-fest/0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + dev: true + + /type-fest/0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + dev: true + + /typechain/8.1.0_typescript@4.8.4: + resolution: {integrity: sha512-5jToLgKTjHdI1VKqs/K8BLYy42Sr3o8bV5ojh4MnR9ExHO83cyyUdw+7+vMJCpKXUiVUvARM4qmHTFuyaCMAZQ==} + hasBin: true + peerDependencies: + typescript: '>=4.3.0' + dependencies: + '@types/prettier': 2.7.1 + debug: 4.3.4 + fs-extra: 7.0.1 + glob: 7.1.7 + js-sha3: 0.8.0 + lodash: 4.17.21 + mkdirp: 1.0.4 + prettier: 2.7.1 + ts-command-line-args: 2.3.1 + ts-essentials: 7.0.3_typescript@4.8.4 + typescript: 4.8.4 + transitivePeerDependencies: + - supports-color + dev: true + + /typedarray/0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: true + + /typescript/4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /typical/4.0.0: + resolution: {integrity: sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==} + engines: {node: '>=8'} + dev: true + + /typical/5.2.0: + resolution: {integrity: sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==} + engines: {node: '>=8'} + dev: true + + /uglify-js/3.17.3: + resolution: {integrity: sha512-JmMFDME3iufZnBpyKL+uS78LRiC+mK55zWfM5f/pWBJfpOttXAqYfdDGRukYhJuyRinvPVAtUhvy7rlDybNtFg==} + engines: {node: '>=0.8.0'} + hasBin: true + requiresBuild: true + dev: true + optional: true + + /unbox-primitive/1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.2 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /undici/5.11.0: + resolution: {integrity: sha512-oWjWJHzFet0Ow4YZBkyiJwiK5vWqEYoH7BINzJAJOLedZ++JpAlCbUktW2GQ2DS2FpKmxD/JMtWUUWl1BtghGw==} + engines: {node: '>=12.18'} + dependencies: + busboy: 1.6.0 + dev: true + + /universalify/0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify/2.0.0: + resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} + engines: {node: '>= 10.0.0'} + dev: true + + /unpipe/1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: true + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: true + + /utf8/3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + dev: true + + /util-deprecate/1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /uuid/2.0.1: + resolution: {integrity: sha512-nWg9+Oa3qD2CQzHIP4qKUqwNfzKn8P0LtFhotaCTFchsV7ZfDhAybeip/HZVeMIpZi9JgY1E3nUlwaCmZT1sEg==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + dev: true + + /uuid/3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: true + + /uuid/8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: true + + /v8-compile-cache-lib/3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /verror/1.10.0: + resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: true + + /web3-utils/1.8.0: + resolution: {integrity: sha512-7nUIl7UWpLVka2f09CMbKOSEvorvHnaugIabU4mj7zfMvm0tSByLcEu3eyV9qgS11qxxLuOkzBIwCstTflhmpQ==} + engines: {node: '>=8.0.0'} + dependencies: + bn.js: 5.2.1 + ethereum-bloom-filters: 1.0.10 + ethereumjs-util: 7.1.5 + ethjs-unit: 0.1.6 + number-to-bn: 1.7.0 + randombytes: 2.1.0 + utf8: 3.0.0 + dev: true + + /which-boxed-primitive/1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-module/2.0.0: + resolution: {integrity: sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==} + dev: true + + /which/1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /wide-align/1.1.3: + resolution: {integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==} + dependencies: + string-width: 2.1.1 + dev: true + + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wordwrap/1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + dev: true + + /wordwrapjs/4.0.1: + resolution: {integrity: sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==} + engines: {node: '>=8.0.0'} + dependencies: + reduce-flatten: 2.0.0 + typical: 5.2.0 + dev: true + + /workerpool/6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi/5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + dev: true + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /ws/7.4.6: + resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /ws/7.5.9: + resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xmlhttprequest/1.8.0: + resolution: {integrity: sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=} + engines: {node: '>=0.4.0'} + dev: true + + /y18n/4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + dev: true + + /y18n/5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yallist/3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + dev: true + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yargs-parser/13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + dev: true + + /yargs-parser/20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-unparser/1.6.0: + resolution: {integrity: sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==} + engines: {node: '>=6'} + dependencies: + flat: 4.1.1 + lodash: 4.17.21 + yargs: 13.3.2 + dev: true + + /yargs-unparser/2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs/13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + dependencies: + cliui: 5.0.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.0 + y18n: 4.0.3 + yargs-parser: 13.1.2 + dev: true + + /yargs/16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /yn/3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true From 2fbadd621ea523c42bbedaf5014fb43f17aa16cb Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 2 Jan 2023 04:00:02 +0200 Subject: [PATCH 098/274] EIP-4337 - AA-100: Change bundling rules to prevent any cross-op access (#6246) --- EIPS/eip-4337.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index da3ef3156e5d73..65b4d0e14cdcc0 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -341,7 +341,8 @@ These UserOperations can be bundled together with UserOperations from the main m During bundling, the client should: -* Exclude UserOps that access any sender address created by another UserOp on the same batch (via a factory). +* Exclude UserOps that access any sender address of another UserOp in the same batch. +* Exclude UserOps that access any address created by another UserOp validation in the same batch (via a 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 From 1dcee3f1235607692053191c2952d1977f57d7a2 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Mon, 2 Jan 2023 01:28:38 -0700 Subject: [PATCH 099/274] 3540: type size range should start at 0 (#6248) --- EIPS/eip-3540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 038500049c4b23..89aa8a109cc51c 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -86,7 +86,7 @@ The EOF header is followed by at least one section header. Each section header c | description | length | value | | |-------------------|---------|---------------|-------------------| | section_kind | 1-byte | 0x01–0xFF | `uint8` | -| section_size | 2-bytes | 0x0001–0xFFFF | `uint16` | +| section_size | 2-bytes | 0x0000–0xFFFF | `uint16` | | section_size_list | dynamic | n/a | `uint16, uint16+` | The list of section headers is terminated with the *section headers terminator byte* `0x00`. The body content follows immediately after. From 82701cd81fcba1238c200ae6865c88a9db36ad69 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Mon, 2 Jan 2023 13:56:45 +0200 Subject: [PATCH 100/274] EIP-4337 AA-99 account returns validAfter,validUntil (#6250) --- EIPS/eip-4337.md | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index 65b4d0e14cdcc0..86ee5bb9705f7a 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -84,11 +84,21 @@ struct UserOpsPerAggregator { } function simulateValidation(UserOperation calldata userOp); -error ValidationResult(uint256 preOpGas, uint256 prefund, uint256 deadline, - StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); - -error ValidationResultWithAggregation(uint256 preOpGas, uint256 prefund, uint256 deadline, - StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, AggregatorStakeInfo aggregatorInfo); +error ValidationResult(ReturnInfo returnInfo, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo); + +error ValidationResultWithAggregation(ReturnInfo returnInfo, + StakeInfo senderInfo, StakeInfo factoryInfo, StakeInfo paymasterInfo, + AggregatorStakeInfo aggregatorInfo); + +struct ReturnInfo { + uint256 preOpGas; + uint256 prefund; + bool sigFailed; + uint64 validAfter; + uint64 validUntil; + bytes paymasterContext; +} struct StakeInfo { uint256 stake; @@ -107,7 +117,7 @@ The core interface required for an account to have is: interface IAccount { function validateUserOp (UserOperation calldata userOp, bytes32 userOpHash, address aggregator, uint256 missingAccountFunds) - external returns (uint256 deadline); + external returns (uint256 sigTimeRange); } ``` @@ -120,9 +130,11 @@ The account * MUST pay the entryPoint (caller) at least the "missingAccountFunds" (which might be zero, in case current account's deposit is high enough) * The account MAY pay more than this minimum, to cover future transactions (it can always issue `withdrawTo` to retrieve it) * The `aggregator` SHOULD be ignored for accounts that don't use an aggregator -* The return value `deadline` is either zero (meaning "indefinitely"), or the last timestamp this request is deemed valid. - (or SIG_VALIDATION_FAILED on signature mismatch) - +* The return value is packed of sigFailure, validUntil and validAfter timestamps. + * `sigFailure` is 1 byte value of "1" the signature check failed (should not revert on signature failure, to support estimate) + * `validUntil` is 8-byte timestamp value, or zero for "infinite". The UserOp is valid only up to this time. + * `validAfter` is 8-byte timestamp. The UserOp is valid only after this time. + An account that works with aggregated signature should have the interface: ```solidity @@ -208,9 +220,9 @@ Maliciously crafted paymasters _can_ DoS the system. To prevent this, we use a r The paymaster interface is as follows: ```c++ -function validatePaymasterUserOp + function validatePaymasterUserOp (UserOperation calldata userOp, bytes32 userOpHash, uint256 maxCost) - external returns (bytes memory context, uint256 deadline); + external returns (bytes memory context, uint256 sigTimeRange); function postOp (PostOpMode mode, bytes calldata context, uint256 actualGasCost) @@ -289,9 +301,9 @@ The simulated call performs the full validation, by calling: 2. `account.validateUserOp`. 3. if specified a paymaster: `paymaster.validatePaymasterUserOp`. -Either `validateUserOp` or `validatePaymasterUserOp` may return a "deadline", which is the latest timestamp that this UserOperation is valid on-chain. -the simulateValidation call returns the minimum of those deadlines. -A node MAY drop a UserOperation if the deadline is too soon (e.g. wouldn't make it to the next block) +Either `validateUserOp` or `validatePaymasterUserOp` may return a "validAfter" and "validUntil" timestamps, which is the time-range that this UserOperation is valid on-chain. +The simulateValidation call returns this range. +A node MAY drop a UserOperation if it expires too soon (e.g. wouldn't make it to the next block) 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 3 functions. @@ -499,8 +511,8 @@ The result `SHOULD` be set to the **userOpHash** if and only if the request pass * The `message` field SHOULD be set to the revert message from the paymaster * The `data` field MUST contain a `paymaster` value * **code: -32502** - transaction rejected because of opcode validation - * **code: -32503** - UserOperation expires shortly: either wallet or paymaster returned an `deadline` that will expire soon - * The `data` field SHOULD contain a `deadline` value + * **code: -32503** - UserOperation out of time-range: either wallet or paymaster returned a time-range, and it is already expired (or will expire soon) + * The `data` field SHOULD contain the `validUntil` and `validAfter` values * The `data` field SHOULD contain a `paymaster` value, if this error was triggered by the paymaster * **code: -32504** - transaction rejected because paymaster (or signature aggregator) is throttled/banned * The `data` field SHOULD contain a `paymaster` or `aggregator` value, depending on the failed entity From d45b70b0ee58960a3e86dea9e83f5ffa6120cba0 Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Tue, 3 Jan 2023 00:05:54 +0200 Subject: [PATCH 101/274] update unstaked entity rules (#6252) --- EIPS/eip-4337.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index 86ee5bb9705f7a..5ca80ed964d3d7 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -382,7 +382,7 @@ Exceptions to the forbidden opcodes: ### Reputation scoring and throttling/banning for global entities -#### ReputationRationale. +#### Reputation Rationale. UserOperation's storage access rules prevent them from interfere with each other. But "global" entities - paymasters, factories and aggregators are accessed by multiple UserOperations, and thus might invalidate multiple previously-valid UserOperations. @@ -390,8 +390,9 @@ But "global" entities - paymasters, factories and aggregators are accessed by mu To prevent abuse, we throttle down (or completely ban for a period of time) an entity that causes invalidation of large number of UserOperations in the mempool. To prevent such entities from "sybil-attack", we require them to stake with the system, and thus make such DoS attack very expensive. Note that this stake is never slashed, and can be withdrawn any time (after unstake delay) -The only exemption from staking is if the entity doesn't use storage during validation. -(unstaked entity may use storage [associated with the sender](#storage-associated-with-an-address)) + +Unstaked entities are allowed, under the rules below. + When staked, an entity is also allowed to use its own associated storage, in addition to sender's associated storage. The stake value is not enforced on-chain, but specifically by each node while simulating a transaction. @@ -399,6 +400,14 @@ The stake is expected to be above MIN_STAKE_VALUE, and unstake delay above MIN_U The value of MIN_UNSTAKE_DELAY is 84600 (one day) The value of MIN_STAKE_VALUE is determined per chain, and specified in the "bundler specification test suite" +#### Un-staked entities + +Under the following special conditions, unstaked entities still can be used: + +- An entity that doesn't use any storage at all, or only the senders's storage (not the entity's storage - that does require a stake) +- If the UserOp doesn't create a new account (that is initCode is empty), then the entity may also use [storage associated with the sender](#storage-associated-with-an-address)) +- A paymaster that has a “postOp()” method (that is, validatePaymasterUserOp returns “context”) must be staked + #### Specification. In the following specification, "entity" is either address that is explicitly referenced by the UserOperation: sender, factory, paymaster and aggregator. From a277b08d87d97ebf9d06ab7f8d770281b1994fe3 Mon Sep 17 00:00:00 2001 From: Zergity <37166829+Zergity@users.noreply.github.com> Date: Tue, 3 Jan 2023 05:07:39 +0700 Subject: [PATCH 102/274] Add EIP-6120: Universal Token Router (#6120) * add templater EIP * fill in informations * spec action and interface * update specs * Reference Implementation and Usage Samples * quote * EVM-compatible networks * more code comments * update Abstract * autho email in angle brackets * correct the description * address PR comments * asign EIP number * code formats * rename title and address PR comments * add discussions topic URL * eipw: markdown-link-first * correct the Output Action number for verification * remove iface in js code * Apply suggestions from code review * add some motivation texts and update new reference implementation * RFC 2119 keywords * add some security related contents * rename all 'Universal Router' to 'Universal Token Router' * update new specs * fix the optional output execution and texts * remove unused UniswapV2Helper01.swapTokensForExactTokens function * update style and correct typos * completely rewritten * status: Review * address CI issues * some text for clarification * address CI issues --- EIPS/eip-6120.md | 517 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 EIPS/eip-6120.md diff --git a/EIPS/eip-6120.md b/EIPS/eip-6120.md new file mode 100644 index 00000000000000..71757b910dfff4 --- /dev/null +++ b/EIPS/eip-6120.md @@ -0,0 +1,517 @@ +--- +eip: 6120 +title: Universal Token Router +description: A single router contract enables tokens to be sent to application contracts in the transfer-and-call manner instead of approve-then-call. +author: Zergity (@Zergity), Zergity +discussions-to: https://ethereum-magicians.org/t/eip-6120-universal-token-router/12142 +status: Review +type: Standards Track +category: ERC +created: 2022-12-12 +requires: 20, 721, 1155 +--- + +## Abstract + +ETH is designed with transfer-and-call as the default behavior in a transaction. Unfortunately, [EIP-20](./eip-20.md) is not designed with that pattern in mind and newer standards are too late to replace it as the de facto standard. + +Application and router contracts have to use the approve-then-call pattern which costs additional `n*m*l` `allow` (or `permit`) transactions, for `n` contracts, `m` tokens, and `l` user addresses. These allowance transactions not only cost enormous amounts of user gas, waste network storage and throughput, and worsen user experience, but also put users at serious security risks as they often have to approve unaudited, unverified and upgradable proxy contracts. + +The Universal Token Router (UTR) separates the token allowance from the application logic, allowing any token to be spent in a contract call the same way with ETH, without approving any other application contracts. + +Tokens approved to the Universal Token Router can only be spent in transactions directly signed by their owner, and they have clearly visible token transfer behavior, including token types (ETH, [EIP-20](./eip-20.md), [EIP-721](./eip-721.md) or [EIP-1155](./eip-1155.md)), `amountInMax`, `amountOutMin`, and `recipient`. + +The Universal Token Router contract is counter-factually deployed using `CREATE2` at a single address across all EVM-compatible networks, so new token contracts can pre-configure it as a trusted spender and no approval transaction is necessary ever again. + +## Motivation + +When users approve their tokens to a contract, they trust that: + +* it only spends the tokens with their permission (usually from `msg.sender` or using `ecrecover`) +* it does not use `delegatecall` (mostly in upgradable proxies) + +By performing the same security conditions above, the Universal Token Router can be shared by all applications, saving `(n-1)*m*l` approval transactions for old tokens and **ALL** approval transactions for new tokens. + +Before this EIP, when users sign transactions to spend their approved tokens, they trust the front-end code entirely to construct those transactions honestly and correctly. This puts them at great risk of phishing sites. + +The Universal Token Router function arguments can act as a manifest for users when signing a transaction. With the support from wallets, users can see and review their expected token behavior instead of blindly trusting the application contracts and front-end code. Phishing sites will be much easier to detect and avoid for users. + +Application contracts follow this standard can use the Universal Token Router to have the following benefits: + +* Share the user token allowance with all other applications. +* Freely update their helper contract logic. +* Save development and security audit costs on router contracts. + +## Specification + +```solidity +struct Token { + uint eip; // token standard: 0 for ETH or EIP number + address adr; // token contract address + uint id; // token id for EIP721 and EIP1155 + uint amount; // amountInMax for input action, amountOutMin for output action + uint offset; // byte offset to get the amountIn from the last inputParams + address recipient; +} +``` + +```solidity +struct Action { + uint output; // 0 for input, 1 for mandatory output, 2 for optional (failable) output + address code; // contract code address + bytes data; // contract input data + Token[] tokens; // tokens to transfer or verify balance +} +``` + +```solidity +interface IUniversalTokenRouter { + function exec(Action[] calldata actions) external payable; +} +``` + +### Input Action + +Actions with `action.output == 0` declare which and how many tokens are transferred from `msg.sender` to `token.recipient`. + +1. If the `action.data` is not empty, `action.code.call(action.data)` is executed and the value returned is recorded in `amountIns` as a `bytes` for subsequence token transfer amounts. +2. For each `token` in `action.tokens`: + * (a) The amount of token to be transferred is determined as: + * if `offset < 32`, `amountIn = token.amount` + * if `offset >= 32`, `amountIn = amountIns.slice(offset-32, offset)` + * `amountIn` MUST NOT greater than `token.amount`, otherwise, transaction will be reverted with `EXCESSIVE_INPUT_AMOUNT` reason. + * (b) If the token is `ETH` and the `recipient` is `0x0`, step **(c)** is skipped and the `amountIn` will be passed to the next output action as the transaction value. + * (c) Transfer `amountIn` of token from `msg.sender` to `recipient`. + +Note: `amountIns` is the last value returned by an input action's code contract call. It can be shared by multiple input actions until another action's code is executed. Using `amountIns` (by passing `offset >= 32`) before any input action execution can produce unexpected `amountIn` value. + +### Output Action + +Actions with `action.output > 0` declare the main application action to execute, and optionally verify the output tokens after all is done. + +1. For each `token` in `action.tokens` with `amount > 0`, the current token balance of `recipient` is recorded for later verification. +2. Execute the `action.code.call{value: value}(action.data)`, where `value` can be zero or the `amountIn` of the last `ETH` input with `recipient == 0x0` (see Input Action 2b). +3. `action.code` execution failure (revert) can be ignored by passing `action.output == 2`. + +The last `amountIns` bytes can be passed to an output action code execution by: + +* using a function with the last param is a `bytes memory` or `bytes calldata` type. +* pass the following value to that last param in the output `action.data`: + `AMOUNT_INS_PLACEHOLDER = keccak256('UniversalTokenRouter.AMOUNT_INS_PLACEHOLDER')` + +### Output Token Verification + +After all the actions are handled as above, every token balance tracked in Output Action #1 is queried again for comparison. The balance change MUST NOT be less than `amount` of each token, otherwise, transaction will be reverted with `INSUFFICIENT_OUTPUT_AMOUNT` reason. + +A special id `EIP_721_ALL = keccak256('UniversalTokenRouter.EIP_721_ALL')` is reserved for EIP-721, which can be used in output actions to verify the total amount of all ids owned by the `recipient` address. + +### Usage Samples + +#### `UniswapRouter.swapExactTokensForTokens` + +Legacy function: + +```solidity +UniswapV2Router01.swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline +) +``` + +This function does what `UniswapV2Router01.swapExactTokensForTokens` does, without the token transfer part: + +```solidity +UniswapV2Helper01.swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline +) +``` + +This transaction is signed by users to execute the swap instead of the legacy function: + +```javascript +UniversalTokenRouter.exec([{ + output: 0, + code: AddressZero, + data: '0x', + tokens: [{ + eip: 20, + token: path[0], + id: 0, + amount: amountIn, + offset: 0, // use amount specified above + recipient: UniswapV2Library.pairFor(factory, path[0], path[1]), + }], +}, { + output: 1, + code: UniswapV2Helper01.address, + data: encodeFunctionData("swapExactTokensForTokens", [amountIn, amountOutMin, path, to, deadline]), + tokens: [{ + eip: 20, + token: path[path.length-1], + id: 0, + amount: amountOutMin, + offset: 0, // unused for output action + recipient: to, + }], +}]) +``` + +#### `UniswapRouter.swapTokensForExactTokens` + +Legacy function: + +```solidity +UniswapV2Router01.swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline +) +``` + +This function accepts the `uint[] amounts` as the last `bytes` param, decode and pass to the internal function `_swap` of `UniswapV2Helper01`. + +```solidity +UniswapV2Helper01.swap(address[] calldata path, address _to, bytes calldata amountsBytes) external { + uint[] memory amounts = abi.decode(amountsBytes, (uint[])); + _swap(amounts, path, _to); +} +``` + +This transaction is signed by users to execute the swap instead of the legacy function: + +```javascript +UniversalTokenRouter.exec([{ + output: 0, + code: UniswapV2Helper01.address, + data: encodeFunctionData("getAmountIns", [amountOut, path]), + tokens: [{ + eip: 20, + token: path[0], + id: 0, + amount: amountInMax, + offset: 64, // first item of getAmountIns result + recipient: UniswapV2Library.pairFor(factory, path[0], path[1]), + }], +}, { + output: 1, + code: UniswapV2Helper01.address, + data: encodeFunctionData("swap", [path, to, AMOUNT_INS_PLACEHOLDER]), + tokens: [{ + eip: 20, + token: path[path.length-1], + id: 0, + amount: amountOut, + offset: 0, // unused for output action + recipient: to, + }], +}]) +``` + +The result of input action's `getAmountIns` will replace the `AMOUNT_INS_PLACEHOLDER` bytes, save the transaction from calculating twice with the same data. + +#### `UniswapRouter.addLiquidity` + +Legacy function: + +```solidity +UniswapV2Router01.addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline +) +``` + +This transaction is signed by users to execute the swap instead of the legacy function: + +```javascript +UniversalTokenRouter.exec([{ + output: 0, + code: UniswapV2Helper01.address, + data: encodeFunctionData("_addLiquidity", [ + tokenA, + tokenB, + amountADesired, + amountBDesired, + amountAMin, + amountBMin, + ]), + tokens: [{ + eip: 20, + token: tokenA, + id: 0, + amount: amountADesired, + offset: 32, // first item of _addLiquidity result + recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB), + }, { + eip: 20, + token: tokenB, + id: 0, + amount: amountBDesired, + offset: 64, // second item of _addLiquidity result + recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB), + }], +}, { + output: 1, + code: UniswapV2Library.pairFor(factory, tokenA, tokenB), + data: encodeFunctionData("mint", [to]), + tokens: [{ + eip: 20, + token: UniswapV2Library.pairFor(factory, tokenA, tokenB), + id: 0, + amount: 1, // just enough to verify the correct recipient + offset: 0, // unused for output action + recipient: to, + }], +}]) +``` + +The `tokens` verification of the last output action is not performed by Uniswap's legacy function and can be skipped. But the output token verification SHOULD always be done for the `UniversalTokenRouter` so user can see and review the token behavior instead of blindly trust the front-end code. + +## Rationale + +The `Permit` type signature is not supported since the purpose of the Universal Token Router is to eliminate all `approve` signatures for new tokens, and *most* for old tokens. + +Flashloan transactions are out of scope since it requires support from the application contracts themself. + +## Backwards Compatibility + +Old token contracts (EIP-20, EIP-721 and EIP-1155) require approval for the Universal Token Router once for each account. + +New token contracts can pre-configure the Universal Token Router as a trusted spender, and no approval transaction is required. + +## Reference Implementation + +```solidity +contract UniversalTokenRouter is IUniversalTokenRouter { + uint constant AMOUNT_INS_PLACEHOLDER = uint(keccak256('UniversalTokenRouter.AMOUNT_INS_PLACEHOLDER')); + uint constant EIP_721_ALL = uint(keccak256('UniversalTokenRouter.EIP_721_ALL')); + + function exec( + Action[] calldata actions + ) override external payable { + unchecked { + uint[][] memory amounts = new uint[][](actions.length); + uint value; // track the ETH value to pass to next output action transaction value + bytes memory amountIns; + for (uint i = 0; i < actions.length; ++i) { + Action memory action = actions[i]; + if (action.output > 0) { + // output action + amounts[i] = new uint[](action.tokens.length); + for (uint j = 0; j < action.tokens.length; ++j) { + Token memory token = action.tokens[j]; + if (token.amount > 0) { + // track the recipient balance before the action is executed + amounts[i][j] = _balanceOf(token); + } + } + if (action.data.length > 0) { + uint length = action.data.length; + if (length >= 4+32*3 && + _sliceUint(action.data, length) == AMOUNT_INS_PLACEHOLDER && + _sliceUint(action.data, length-32) == 32) + { + action.data = _concat(action.data, length-32, amountIns); + } + (bool success, bytes memory result) = action.code.call{value: value}(action.data); + // ignore output action error if output == 2 + if (!success && action.output == 2) { + assembly { + revert(add(result,32),mload(result)) + } + } + delete value; // clear the ETH value after transfer + } + continue; + } + // input action + if (action.data.length > 0) { + bool success; + (success, amountIns) = action.code.call(action.data); + if (!success) { + assembly { + revert(add(amountIns,32),mload(amountIns)) + } + } + } + for (uint j = 0; j < action.tokens.length; ++j) { + Token memory token = action.tokens[j]; + if (token.offset >= 32) { + uint amount = _sliceUint(amountIns, token.offset); + require(amount <= token.amount, "UniversalTokenRouter: EXCESSIVE_INPUT_AMOUNT"); + token.amount = amount; + } + if (token.eip == 0 && token.recipient == address(0x0)) { + value = token.amount; + continue; // ETH not transfered here will be passed to the next output call value + } + if (token.amount > 0) { + _transfer(token); + } + } + } + // refund any left-over ETH + uint leftOver = address(this).balance; + if (leftOver > 0) { + TransferHelper.safeTransferETH(msg.sender, leftOver); + } + // verify the balance change + for (uint i = 0; i < actions.length; ++i) { + if (actions[i].output == 0) { + continue; + } + for (uint j = 0; j < actions[i].tokens.length; ++j) { + Token memory token = actions[i].tokens[j]; + if (token.amount == 0) { + continue; + } + uint balance = _balanceOf(token); + uint change = balance - amounts[i][j]; // overflow checked with `change <= balance` bellow + require(change >= token.amount && change <= balance, 'UniversalTokenRouter: INSUFFICIENT_OUTPUT_AMOUNT'); + amounts[i][j] = change; + } + } + } } + + function _transfer(Token memory token) internal { + unchecked { + if (token.eip == 20) { + TransferHelper.safeTransferFrom(token.adr, msg.sender, token.recipient, token.amount); + } else if (token.eip == 1155) { + IERC1155(token.adr).safeTransferFrom(msg.sender, token.recipient, token.id, token.amount, ""); + } else if (token.eip == 721) { + IERC721(token.adr).safeTransferFrom(msg.sender, token.recipient, token.id); + } else if (token.eip == 0) { + TransferHelper.safeTransferETH(token.recipient, token.amount); + } else { + revert("UniversalTokenRouter: INVALID_EIP"); + } + } } + + function _balanceOf(Token memory token) internal view returns (uint balance) { + unchecked { + if (token.eip == 20) { + return IERC20(token.adr).balanceOf(token.recipient); + } + if (token.eip == 1155) { + return IERC1155(token.adr).balanceOf(token.recipient, token.id); + } + if (token.eip == 721) { + if (token.id == EIP_721_ALL) { + return IERC721(token.adr).balanceOf(token.recipient); + } + return IERC721(token.adr).ownerOf(token.id) == token.recipient ? 1 : 0; + } + if (token.eip == 0) { + return token.recipient.balance; + } + revert("UniversalTokenRouter: INVALID_EIP"); + } } + + // https://ethereum.stackexchange.com/a/54405 + function _sliceUint(bytes memory bs, uint start) internal pure returns (uint x) { + unchecked { + // require(bs.length >= start + 32, "slicing out of range"); + assembly { + x := mload(add(bs, start)) + } + } } + + // https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol + function _concat( + bytes memory preBytes, + uint length, + bytes memory postBytes + ) + internal + pure + returns (bytes memory bothBytes) + { + assembly { + // Get a location of some free memory and store it in bothBytes as + // Solidity does for memory variables. + bothBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for bothBytes. + mstore(bothBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(bothBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the preBytes data, + // 32 bytes into its memory. + let cc := add(preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the preBytes data into the bothBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of postBytes to the current length of bothBytes + // and store it as the new length in the first 32 bytes of the + // bothBytes memory. + length := mload(postBytes) + mstore(bothBytes, add(length, mload(bothBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the preBytes data. + mc := sub(end, 0x20) + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(end, length) + + for { + let cc := postBytes + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of bothBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore(0x40, and( + add(add(end, iszero(add(length, mload(preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + )) + } + } +} +``` + +## Security Considerations + +As long as user tokens are only transferred from `msg.sender`, the token allowance can only be used with transactions signed by the token owner. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 788baf9f005ca7cebdd6e07cf39576aaac61ab0b Mon Sep 17 00:00:00 2001 From: Yamen Merhi Date: Tue, 3 Jan 2023 14:28:09 +0200 Subject: [PATCH 103/274] Update EIP-725: Improvements (#6103) * update eip-725 standard * add example code in assets folder * resolve ci warnings * update pragma version for eip725 assets * resolve other ci warnings * resolve more ci warnings * replace issue with ethereum magicians link --- EIPS/eip-725.md | 342 ++++++---- assets/eip-725/ERC725.sol | 1283 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1506 insertions(+), 119 deletions(-) create mode 100644 assets/eip-725/ERC725.sol diff --git a/EIPS/eip-725.md b/EIPS/eip-725.md index 5d5e90f4c7a4a6..08452e2464d570 100644 --- a/EIPS/eip-725.md +++ b/EIPS/eip-725.md @@ -1,86 +1,165 @@ --- eip: 725 -title: General key-value store and execution standard +title: General data key/value store and execution +description: An interface for a smart contract based account with attachable data key/value store author: Fabian Vogelsteller (@frozeman), Tyler Yasaka (@tyleryasaka) -discussions-to: https://github.com/ethereum/EIPs/issues/725 -status: Stagnant +discussions-to: https://ethereum-magicians.org/t/discussion-for-eip725/12158 +status: Draft type: Standards Track category: ERC -requires: 165, 173 created: 2017-10-02 +requires: 165, 173 --- -## Simple Summary +## Abstract -A standard interface for a smart contract based account with attachable key value store. +The following describes two standards that allow for a generic data storage in a smart contract and a generic execution through a smart contract. These can be used separately or in conjunction and can serve as building blocks for smart contract accounts, upgradable metadata, and other means. -## Abstract +## Motivation -The following describes standard functions for a unique smart contract based account that can be used by humans, -groups, organisations, objects and machines. +The initial motivation came out of the need to create a smart contract account system that's flexible enough to be viable long-term but also defined enough to be standardized. They are a generic set of two standardized building blocks to be used in all forms of smart contracts. -The standard is divided into two sub standards: +This standard consists of two sub-standards, a generic data key/value store (`ERC725Y`) and a generic execute function (`ERC725X`). Both of these in combination allow for a very flexible and long-lasting account system. The account version of `ERC725` is standardized under `LSP0-ERC725Account`. -`ERC725X`: -Can execute arbitrary smart contracts and deploy other smart contracts. +These standards (`ERC725` X and Y) can also be used separately as `ERC725Y` can be used to enhance NFTs and Token metadata or other types of smart contracts. `ERC725X` allows for a generic execution through a smart contract, functioning as an account or actor. -`ERC725Y`: -Can hold arbitrary data through a generic key/value store. +## Specification -## Motivation +### Ownership -Standardizing a minimal interface for a smart contract based account allows any interface to operate through these account types. -Smart contact based accounts following this standard have the following advantages: +This contract is controlled by a single owner. The owner can be a smart contract or an external account. +This standard requires [EIP-173](./eip-173.md) and SHOULD implement the functions: -- can hold any asset (native token, e.g. ERC20 like tokens) -- can execute any smart contract and deploy smart contracts -- have upgradeable security (through owner change, e.g. to a gnosis safe) -- are basic enough to work for for a long time -- are extensible though additional standardisation of the key/value data. -- can function as an owner/controller or proxy of other smart contracts +- `owner() view` +- `transferOwnership(address newOwner)` -## Specification +And the event: + +- `OwnershipTransferred(address indexed previousOwner, address indexed newOwner)` + +--- + +### `ERC725X` + +**`ERC725X`** interface id according to [EIP-165](./eip-165.md): `0x570ef073`. + +Smart contracts implementing the `ERC725X` standard MUST implement the [EIP-165](./eip-165.md) `supportsInterface(..)` function and MUST support the `ERC165` and `ERC725X` interface ids. -### ERC725X +### `ERC725X` Methods -ERC165 identifier: `0x44c028fe` +Smart contracts implementing the `ERC725X` standard SHOULD implement all of the functions listed below: #### execute ```solidity -function execute(uint256 operationType, address to, uint256 value, bytes calldata data) public payable returns(bytes memory) +function execute(uint256 operationType, address target, uint256 value, bytes memory data) external payable returns(bytes memory) ``` -Executes a call on any other smart contracts, transfers the blockchains native token, or deploys a new smart contract. -MUST only be called by the current owner of the contract. +Function Selector: `0x44c028fe` + +Executes a call on any other smart contracts or address, transfers the blockchains native token, or deploys a new smart contract. + _Parameters:_ -- `operationType`: the operation to execute. -- `to`: the smart contract or address to interact with. `to` will be unused if a contract is created (operation 1 and 2). -- `value`: the value of ETH to transfer. -- `data`: the call data, or the contract data to deploy. +- `operationType`: the operation type used to execute. +- `target`: the smart contract or address to call. `target` will be unused if a contract is created (operation types 1 and 2). +- `value`: the amount of native tokens to transfer (in Wei). +- `data`: the call data, or the creation bytecode of the contract to deploy. -_Returns:_ `bytes` , the returned data of the called function, or the address of the contract created (operation 1 and 2). -The `operationType` can be the following: +_Requirements:_ -- `0` for `call` -- `1` for `create` -- `2` for `create2` -- `3` for `staticcall` -- `4` for `delegatecall` +- MUST only be called by the current owner of the contract. +- MUST revert when the execution or the contract creation fails. +- `target` SHOULD be address(0) in case of contract creation with `CREATE` and `CREATE2` (operation types 1 and 2). +- `value` SHOULD be zero in case of `STATICCALL` or `DELEGATECALL` (operation types 3 and 4). -Others may be added in the future. + +_Returns:_ `bytes` , the returned data of the called function, or the address of the contract deployed (operation types 1 and 2). **Triggers Event:** [ContractCreated](#contractcreated), [Executed](#executed) -### Events +The following `operationType` COULD exist: + +- `0` for `CALL` +- `1` for `CREATE` +- `2` for `CREATE2` +- `3` for `STATICCALL` +- `4` for `DELEGATECALL` - **NOTE** This is a potentially dangerous operation type + +Others may be added in the future. + +#### data parameter + +- For operationType, `CALL`, `STATICCALL` and `DELEGATECALL` the data field can be random bytes or an abi-encoded function call. + +- For operationType, `CREATE` the `data` field is the creation bytecode of the contract to deploy appended with the constructor argument(s) abi-encoded. + +- For operationType, `CREATE2` the `data` field is the creation bytecode of the contract to deploy appended with: + 1. the constructor argument(s) abi-encoded + 2. a `bytes32` salt. + +``` +data = + + +``` + +> See [EIP-1014: Skinny CREATE2](./eip-1014.md) for more information. + +#### execute (Array) + +```solidity +function execute(uint256[] memory operationsType, address[] memory targets, uint256[] memory values, bytes[] memory datas) external payable returns(bytes[] memory) +``` + +Function Selector: `0x13ced88d` + +Executes a batch of calls on any other smart contracts, transfers the blockchain native token, or deploys a new smart contract. + +_Parameters:_ + +- `operationsType`: the list of operations type used to execute. +- `targets`: the list of addresses to call. `targets` will be unused if a contract is created (operation types 1 and 2). +- `values`: the list of native token amounts to transfer (in Wei). +- `datas`: the list of call data, or the creation bytecode of the contract to deploy. + +_Requirements:_ + +- Parameters array MUST have the same length. +- MUST only be called by the current owner of the contract. +- MUST revert when the execution or the contract creation fails. +- `target` SHOULD be address(0) in case of contract creation with `CREATE` and `CREATE2` (operation types 1 and 2). +- `value` SHOULD be zero in case of `STATICCALL` or `DELEGATECALL` (operation types 3 and 4). + +_Returns:_ `bytes[]` , array list of returned data of the called function, or the address(es) of the contract deployed (operation types 1 and 2). + +**Triggers Event:** [ContractCreated](#contractcreated), [Executed](#executed) on each call iteration + + +**Note:** The `execute()` functions use function overloading, therefore it is better to reference them by the given function signature as follows: + +```js +// web3.js example + +// execute +myContract.methods['execute(uint256,address,uint256,bytes)'](OPERATION_CALL, target.address, 2WEI, "0x").send(); +// execute Array +myContract.methods['execute(uint256[],address[],uint256[],bytes[])']([OPERATION_CALL, OPERATION_CREATE], [target.address, ZERO_ADDRESS], [2WEI, 0WEI], ["0x", CONTRACT_BYTECODE]).send(); + +// OR + +// execute +myContract.methods['0x44c028fe'](OPERATION_CALL, target.address, 2WEI, "0x").send(); +// execute Array +myContract.methods['0x13ced88d']([OPERATION_CALL, OPERATION_CREATE], [target.address, ZERO_ADDRESS], [2WEI, 0WEI], ["0x", CONTRACT_BYTECODE]).send(); +``` + +### `ERC725X` Events #### Executed ```solidity -event Executed(uint256 indexed _operation, address indexed _to, uint256 indexed _value, bytes _data); +event Executed(uint256 indexed operationType, address indexed target, uint256 indexed value, bytes4 data); ``` MUST be triggered when `execute` creates a new call using the `operationType` `0`, `3`, `4`. @@ -88,162 +167,187 @@ MUST be triggered when `execute` creates a new call using the `operationType` `0 #### ContractCreated ```solidity -event ContractCreated(uint256 indexed _operation, address indexed _contractAddress, uint256 indexed _value); +event ContractCreated(uint256 indexed operationType, address indexed contractAddress, uint256 indexed value, bytes32 salt); ``` MUST be triggered when `execute` creates a new contract using the `operationType` `1`, `2`. -### ERC725Y +--- -ERC165 identifier: `0x714df77c` +### `ERC725Y` -#### setData +**`ERC725Y`** interface id according to [EIP-165](./eip-165.md): `0x714df77c`. + +Smart contracts implementing the `ERC725Y` standard MUST implement the [EIP-165](./eip-165.md) `supportsInterface(..)` function and MUST support the `ERC165` and `ERC725Y` interface ids. + +### `ERC725Y` Methods + +Smart contracts implementing the `ERC725Y` standard MUST implement all of the functions listed below: + +#### getData ```solidity -function setData(bytes32 key, bytes memory value) public +function getData(bytes32 dataKey) external view returns(bytes memory) ``` -Sets data as bytes in the storage for a single key. MUST only be called by the current owner of the contract. +Function Selector: `0x54f6127f` + +Gets the data set for the given data key. _Parameters:_ -- `key`: the key which value to set. -- `value`: the data to set. +- `dataKey`: the data key which value to retrieve. -**Triggers Event:** [DataChanged](#datachanged) -#### setData (Array) +_Returns:_ `bytes` , The data for the requested data key. + +#### getData (Array) ```solidity -function setData(bytes32[] memory keys, bytes[] memory values) public +function getData(bytes32[] memory dataKeys) external view returns(bytes[] memory) ``` -Sets array of data at multiple keys. MUST only be called by the current owner of the contract. +Function Selector: `0x4e3e6e9c` + +Gets array of data at multiple given data keys. _Parameters:_ -- `keys`: the keys which values to set. -- `values`: the array of bytes to set. +- `dataKeys`: the data keys which values to retrieve. -**Triggers Event:** [DataChanged](#datachanged) -#### getData +_Returns:_ `bytes[]` , array of data values for the requested data keys. + +#### setData ```solidity -function getData(bytes32 key) public view returns(bytes memory) +function setData(bytes32 dataKey, bytes memory dataValue) external ``` -Gets the data set for the given key. +Function Selector: `0x7f23690c` + +Sets data as bytes in the storage for a single data key. _Parameters:_ -- `key`: the key which value to retrieve. +- `dataKey`: the data key which value to set. +- `dataValue`: the data to store. -_Returns:_ `bytes` , The data for the requested key. -#### getData (Array) +_Requirements:_ + +- MUST only be called by the current owner of the contract. + +**Triggers Event:** [DataChanged](#datachanged) + +#### setData (Array) ```solidity -function getData(bytes32[] memory keys) public view returns(bytes[] memory) +function setData(bytes32[] memory dataKeys, bytes[] memory dataValues) external ``` -Gets array of data at multiple given key. +Function Selector: `0x14a6e293` + +Sets array of data at multiple data keys. MUST only be called by the current owner of the contract. _Parameters:_ -- `keys`: the keys which values to retrieve. +- `dataKeys`: the data keys which values to set. +- `dataValues`: the array of bytes to set. -_Returns:_ `bytes[]` , array of the values for the requested keys. +_Requirements:_ +- Array parameters MUST have the same length. +- MUST only be called by the current owner of the contract. +**Triggers Event:** [DataChanged](#datachanged) +**Note:** `setData()` and `getData()` uses function overloading, therefore it is better to reference them by the given function signature as follows: -### Events +```js +// web3.js example -#### DataChanged +// setData +myContract.methods['setData(bytes32,bytes)'](dataKey, dataValue).send(); +// setData Array +myContract.methods['setData(bytes32[],bytes[])']([dataKeys, ...], [dataValues, ...]).send(); -```solidity -event DataChanged(bytes32 indexed key, bytes value) +// OR + +// setData +myContract.methods['0x7f23690c'](dataKey, dataValue).send(); +// setData Array +myContract.methods['0x14a6e293']([dataKeys, ...], [dataValues, ...]).send(); ``` -MUST be triggered when `setData` was successfully called. +### `ERC725Y` Events -### Ownership - -This contract is controlled by an owner. The owner can be a smart contract or an external account. -This standard requires [ERC173](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-173.md) and should implement the functions: +#### DataChanged -- `owner() view` -- `transferOwnership(address newOwner)`
+```solidity +event DataChanged(bytes32 indexed dataKey, bytes dataValue) +``` -The event: +MUST be triggered when a data key was successfully set. -- `OwnershipTransferred(address indexed previousOwner, address indexed newOwner)` +### `ERC725Y` Data keys -### Data keys +Data keys, are the way to retrieve values via `getData()`. These `bytes32` values can be freely chosen, or defined by a standard. +A common way to define data keys is the hash of a word, e.g. `keccak256('ERCXXXMyNewKeyType')` which results in: `0x6935a24ea384927f250ee0b954ed498cd9203fc5d2bf95c735e52e6ca675e047` -Data keys, should be the keccak256 hash of a type name. -e.g. `keccak256('ERCXXXMyNewKeyType')` is `0x6935a24ea384927f250ee0b954ed498cd9203fc5d2bf95c735e52e6ca675e047` +The `LSP2-ERC725JSONSchema` standard is a more explicit `ERC725Y` data key standard, that defines key types and value types, and their encoding and decoding. -The [ERC725JSONSchema standard](https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-2-ERC725YJSONSchema.md) defines how keys should be named and generated. -This JSON schema can be used to auto decode ERC725Y values from smart contracts for application and smart contract interactions. +## Rationale -#### Default key values +The generic way of storing data keys with values was chosen to allow upgradability over time. Stored data values can be changed over time. Other smart contract protocols can then interpret this data in new ways and react to interactions from a `ERC725` smart contract differently. -ERC725 key standards need to be defined within new standards, we suggest the following defaults: +The data stored in an `ERC725Y` smart contract is not only readable/writable by off-chain applications, but also by other smart contracts. Function overloading was used to allow for the retrievable of single and multiple keys, to keep gas costs minimal for both use cases. -| Name | Description | Key | value | -| ---------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | -| SupportedStandards:XYZ | Allows to determine standards supported by this contract | `0xeafec4d89fa9619884b6b89135626455000000000000000000000000xxxxxxxx`, where `xxxxxxxx` is the 4 bytes identifier of the standard supported | Value can be defined by the standard, by default it should be the 4 bytes identifier e.g. `0x7a30e6fc` | +## Backwards Compatibility -#### ERC725Account +All contracts since `ERC725v2` from 2018/19 should be compatible with the current version of the standard. Mainly interface ID and Event parameters have changed, while `getData(bytes32[])` and `setData(bytes32[], bytes[])` was added as an efficient way to set/get multiple keys at once. The same applies to execution, as `execute(..[])` was added as an efficient way to batch calls. -The specification of an ERC725Account can be found in [LSP0-ERC725Account](https://github.com/lukso-network/LIPs/blob/master/LSPs/LSP-0-ERC725Account.md). +## Reference Implementation -## Rationale +Reference implementations can be found in [`ERC725.sol`](../assets/eip-725/ERC725.sol). -The purpose of an smart contract account is to allow an entity to exist as a first-class citizen with the ability to execute arbitrary contract calls. +## Security Considerations -## Implementation +This contract allows generic executions, therefore special care needs to be taken to prevent re-entrancy attacks and other forms of call chain attacks. -- [Latest implementation](https://github.com/ERC725Alliance/ERC725/tree/master/implementations/contracts) +When using the operation type `4` for `delegatecall`, it is important to consider that the called contracts can alter the state of the calling contract and also change owner variables and `ERC725Y` data storage entries at will. Additionally calls to `selfdestruct` are possible and other harmful state-changing operations. ### Solidity Interfaces ```solidity // SPDX-License-Identifier: CC0-1.0 + pragma solidity >=0.5.0 <0.7.0; -//ERC165 identifier: `0x44c028fe` +// ERC165 identifier: `0x570ef073` interface IERC725X /* is ERC165, ERC173 */ { - event ContractCreated(uint256 indexed operation, address indexed contractAddress, uint256 indexed value); - event Executed(uint256 indexed operation, address indexed to, uint256 indexed value, bytes data); - function execute(uint256 operationType, address to, uint256 value, bytes calldata data) external payable returns(bytes memory); + event Executed(uint256 indexed operationType, address indexed target, uint256 indexed value, bytes4 data); + event ContractCreated(uint256 indexed operationType, address indexed contractAddress, uint256 indexed value, bytes32 salt); + + + function execute(uint256 operationType, address target, uint256 value, bytes memory data) external payable returns(bytes memory); + + function execute(uint256[] memory operationsType, address[] memory targets, uint256[] memory values, bytes memory datas) external payable returns(bytes[] memory); } -//ERC165 identifier: `0x714df77c` +// ERC165 identifier: `0x714df77c` interface IERC725Y /* is ERC165, ERC173 */ { - event DataChanged(bytes32 indexed key, bytes value); + + event DataChanged(bytes32 indexed dataKey, bytes dataValue); - function setData(bytes32 key, bytes memory value) external; - function setData(bytes32[] memory keys, bytes[] memory values) external; - function getData(bytes32 key) external view returns(bytes memory); - function getData(bytes32[] memory keys) external view returns(bytes[] memory); -} + function getData(bytes32 dataKey) external view returns(bytes memory); + function getData(bytes32[] memory dataKeys) external view returns(bytes[] memory); + function setData(bytes32 dataKey, bytes memory dataValue) external; + function setData(bytes32[] memory dataKeys, bytes[] memory dataValues) external; +} interface IERC725 /* is IERC725X, IERC725Y */ { - } - ``` -## Additional References - -- [Slides of the ERC Identity presentation](https://www.slideshare.net/FabianVogelsteller/erc-725-identity) -- [In-contract claim VS claim registry](https://github.com/ethereum/wiki/wiki/ERC-735:-Claim-Holder-Registry-vs.-in-contract) -- [Identity related reports](https://www.weboftrust.info/specs.html) -- [W3C Verifiable Claims Use Cases](https://w3c.github.io/vc-use-cases/) -- [Decentralised Identity Foundation](https://identity.foundation) -- [Sovrin Foundation Self Sovereign Identity](https://sovrin.org/wp-content/uploads/2017/06/The-Inevitable-Rise-of-Self-Sovereign-Identity.pdf) - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-725/ERC725.sol b/assets/eip-725/ERC725.sol new file mode 100644 index 00000000000000..fbab76f8ef12f1 --- /dev/null +++ b/assets/eip-725/ERC725.sol @@ -0,0 +1,1283 @@ +pragma solidity ^0.8.8; + +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding [ERC165] standard. + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +contract ERC165 is IERC165 { + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override + returns (bool) + { + return interfaceId == type(IERC165).interfaceId; + } +} + +contract ERC173 { + address private _owner; + + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + + /** + * @dev Initializes the contract setting the deployer as the initial owner. + */ + constructor() { + _transferOwnership(msg.sender); + } + + /** + * @dev Throws if called by any account other than the owner. + */ + modifier onlyOwner() { + _checkOwner(); + _; + } + + /** + * @dev Returns the address of the current owner. + */ + function owner() public view virtual returns (address) { + return _owner; + } + + /** + * @dev Throws if the sender is not the owner. + */ + function _checkOwner() internal view virtual { + require(owner() == msg.sender, "Ownable: caller is not the owner"); + } + + /** + * @dev Leaves the contract without owner. It will not be possible to call + * `onlyOwner` functions anymore. Can only be called by the current owner. + * + * NOTE: Renouncing ownership will leave the contract without an owner, + * thereby removing any functionality that is only available to the owner. + */ + function renounceOwnership() public virtual onlyOwner { + _transferOwnership(address(0)); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Can only be called by the current owner. + */ + function transferOwnership(address newOwner) public virtual onlyOwner { + require( + newOwner != address(0), + "Ownable: new owner is the zero address" + ); + _transferOwnership(newOwner); + } + + /** + * @dev Transfers ownership of the contract to a new account (`newOwner`). + * Internal function without access restriction. + */ + function _transferOwnership(address newOwner) internal virtual { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } +} + +interface IERC725X is IERC165 { + /** + * @notice Emitted when deploying a contract + * @param operationType The opcode used to deploy the contract (CREATE or CREATE2) + * @param contractAddress The created contract address + * @param value The amount of native tokens (in Wei) sent to fund the created contract address + * @param salt The salt used in case of CREATE2. Will be bytes32(0) in case of CREATE operation + */ + event ContractCreated( + uint256 indexed operationType, + address indexed contractAddress, + uint256 indexed value, + bytes32 salt + ); + + /** + * @notice Emitted when calling an address (EOA or contract) + * @param operationType The low-level call opcode used to call the `to` address (CALL, STATICALL or DELEGATECALL) + * @param target The address to call. `target` will be unused if a contract is created (operation types 1 and 2). + * @param value The amount of native tokens transferred with the call (in Wei) + * @param selector The first 4 bytes (= function selector) of the data sent with the call + */ + event Executed( + uint256 indexed operationType, + address indexed target, + uint256 indexed value, + bytes4 selector + ); + + /** + * @param operationType The operation type used: CALL = 0; CREATE = 1; CREATE2 = 2; STATICCALL = 3; DELEGATECALL = 4 + * @param target The address of the EOA or smart contract. (unused if a contract is created via operation type 1 or 2) + * @param value The amount of native tokens to transfer (in Wei) + * @param data The call data, or the creation bytecode of the contract to deploy + * + * @dev Generic executor function to: + * + * - send native tokens to any address. + * - interact with any contract by passing an abi-encoded function call in the `data` parameter. + * - deploy a contract by providing its creation bytecode in the `data` parameter. + * + * Requirements: + * + * - SHOULD only be callable by the owner of the contract set via ERC173. + * - if a `value` is provided, the contract MUST have at least this amount in its balance to execute successfully. + * - if the operation type is STATICCALL or DELEGATECALL, `value` SHOULD be 0. + * - `target` SHOULD be address(0) when deploying a contract. + * + * Emits an {Executed} event, when a call is made with `operationType` 0 (CALL), 3 (STATICCALL) or 4 (DELEGATECALL) + * Emits a {ContractCreated} event, when deploying a contract with `operationType` 1 (CREATE) or 2 (CREATE2) + */ + function execute( + uint256 operationType, + address target, + uint256 value, + bytes memory data + ) external payable returns (bytes memory); + + /** + * @param operationsType The list of operations type used: CALL = 0; CREATE = 1; CREATE2 = 2; STATICCALL = 3; DELEGATECALL = 4 + * @param targets The list of addresses to call. `targets` will be unused if a contract is created (operation types 1 and 2). + * @param values The list of native token amounts to transfer (in Wei) + * @param datas The list of call data, or the creation bytecode of the contract to deploy + * + * @dev Generic batch executor function to: + * + * - send native tokens to any address. + * - interact with any contract by passing an abi-encoded function call in the `datas` parameter. + * - deploy a contract by providing its creation bytecode in the `datas` parameter. + * + * Requirements: + * + * - The length of the parameters provided MUST be equal + * - SHOULD only be callable by the owner of the contract set via ERC173. + * - if a `values` is provided, the contract MUST have at least this amount in its balance to execute successfully. + * - if the operation type is STATICCALL or DELEGATECALL, `values` SHOULD be 0. + * - `targets` SHOULD be address(0) when deploying a contract. + * + * Emits an {Executed} event, when a call is made with `operationType` 0 (CALL), 3 (STATICCALL) or 4 (DELEGATECALL) + * Emits a {ContractCreated} event, when deploying a contract with `operationType` 1 (CREATE) or 2 (CREATE2) + */ + function execute( + uint256[] memory operationsType, + address[] memory targets, + uint256[] memory values, + bytes[] memory datas + ) external payable returns (bytes[] memory); +} + +// ERC165 INTERFACE IDs +bytes4 constant _INTERFACEID_ERC725X = 0x570ef073; + +// ERC725X OPERATION TYPES +uint256 constant OPERATION_0_CALL = 0; +uint256 constant OPERATION_1_CREATE = 1; +uint256 constant OPERATION_2_CREATE2 = 2; +uint256 constant OPERATION_3_STATICCALL = 3; +uint256 constant OPERATION_4_DELEGATECALL = 4; + +/** + * @dev reverts when trying to send more native tokens `value` than available in current `balance`. + * @param balance the balance of the ERC725X contract. + * @param value the amount of native tokens sent via `ERC725X.execute(...)`. + */ +error ERC725X_InsufficientBalance(uint256 balance, uint256 value); + +/** + * @dev reverts when the `operationTypeProvided` is none of the default operation types available. + * (CALL = 0; CREATE = 1; CREATE2 = 2; STATICCALL = 3; DELEGATECALL = 4) + */ +error ERC725X_UnknownOperationType(uint256 operationTypeProvided); + +/** + * @dev the `value` parameter (= sending native tokens) is not allowed when making a staticcall + * via `ERC725X.execute(...)` because sending native tokens is a state changing operation. + */ +error ERC725X_MsgValueDisallowedInStaticCall(); + +/** + * @dev the `value` parameter (= sending native tokens) is not allowed when making a delegatecall + * via `ERC725X.execute(...)` because msg.value is persisting. + */ +error ERC725X_MsgValueDisallowedInDelegateCall(); + +/** + * @dev reverts when passing a `to` address while deploying a contract va `ERC725X.execute(...)` + * whether using operation type 1 (CREATE) or 2 (CREATE2). + */ +error ERC725X_CreateOperationsRequireEmptyRecipientAddress(); + +/** + * @dev reverts when contract deployment via `ERC725X.execute(...)` failed. + * whether using operation type 1 (CREATE) or 2 (CREATE2). + */ +error ERC725X_ContractDeploymentFailed(); + +/** + * @dev reverts when no contract bytecode was provided as parameter when trying to deploy a contract + * via `ERC725X.execute(...)`, whether using operation type 1 (CREATE) or 2 (CREATE2). + */ +error ERC725X_NoContractBytecodeProvided(); + +/** + * @dev reverts when there is not the same number of operation, to addresses, value, and data. + */ +error ERC725X_ExecuteParametersLengthMismatch(); + +contract ERC725X is ERC173, ERC165, IERC725X { + /** + * @inheritdoc ERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC165) + returns (bool) + { + return + interfaceId == _INTERFACEID_ERC725X || + super.supportsInterface(interfaceId); + } + + /** + * @inheritdoc IERC725X + */ + function execute( + uint256 operationType, + address target, + uint256 value, + bytes memory data + ) public payable virtual onlyOwner returns (bytes memory) { + if (address(this).balance < value) { + revert ERC725X_InsufficientBalance(address(this).balance, value); + } + return _execute(operationType, target, value, data); + } + + /** + * @inheritdoc IERC725X + */ + function execute( + uint256[] memory operationsType, + address[] memory targets, + uint256[] memory values, + bytes[] memory datas + ) public payable virtual onlyOwner returns (bytes[] memory result) { + if ( + operationsType.length != targets.length || + (targets.length != values.length || values.length != datas.length) + ) revert ERC725X_ExecuteParametersLengthMismatch(); + + result = new bytes[](operationsType.length); + for (uint256 i = 0; i < operationsType.length; i++) { + if (address(this).balance < values[i]) + revert ERC725X_InsufficientBalance( + address(this).balance, + values[i] + ); + + result[i] = _execute( + operationsType[i], + targets[i], + values[i], + datas[i] + ); + } + } + + function _execute( + uint256 operationType, + address target, + uint256 value, + bytes memory data + ) internal virtual returns (bytes memory) { + // CALL + if (operationType == OPERATION_0_CALL) { + return _executeCall(target, value, data); + } + + // Deploy with CREATE + if (operationType == uint256(OPERATION_1_CREATE)) { + if (target != address(0)) + revert ERC725X_CreateOperationsRequireEmptyRecipientAddress(); + return _deployCreate(value, data); + } + + // Deploy with CREATE2 + if (operationType == uint256(OPERATION_2_CREATE2)) { + if (target != address(0)) + revert ERC725X_CreateOperationsRequireEmptyRecipientAddress(); + return _deployCreate2(value, data); + } + + // STATICCALL + if (operationType == uint256(OPERATION_3_STATICCALL)) { + if (value != 0) revert ERC725X_MsgValueDisallowedInStaticCall(); + return _executeStaticCall(target, data); + } + + // DELEGATECALL + // + // WARNING! delegatecall is a dangerous operation type! use with EXTRA CAUTION + // + // delegate allows to call another deployed contract and use its functions + // to update the state of the current calling contract. + // + // this can lead to unexpected behaviour on the contract storage, such as: + // - updating any state variables (even if these are protected) + // - update the contract owner + // - run selfdestruct in the context of this contract + // + if (operationType == uint256(OPERATION_4_DELEGATECALL)) { + if (value != 0) revert ERC725X_MsgValueDisallowedInDelegateCall(); + return _executeDelegateCall(target, data); + } + + revert ERC725X_UnknownOperationType(operationType); + } + + /** + * @dev perform low-level call (operation type = 0) + * @param target The address on which call is executed + * @param value The value to be sent with the call + * @param data The data to be sent with the call + * @return result The data from the call + */ + function _executeCall( + address target, + uint256 value, + bytes memory data + ) internal virtual returns (bytes memory result) { + emit Executed(OPERATION_0_CALL, target, value, bytes4(data)); + + // solhint-disable avoid-low-level-calls + (bool success, bytes memory returnData) = target.call{value: value}( + data + ); + result = Address.verifyCallResult( + success, + returnData, + "ERC725X: Unknown Error" + ); + } + + /** + * @dev perform low-level staticcall (operation type = 3) + * @param target The address on which staticcall is executed + * @param data The data to be sent with the staticcall + * @return result The data returned from the staticcall + */ + function _executeStaticCall(address target, bytes memory data) + internal + virtual + returns (bytes memory result) + { + emit Executed(OPERATION_3_STATICCALL, target, 0, bytes4(data)); + + // solhint-disable avoid-low-level-calls + (bool success, bytes memory returnData) = target.staticcall(data); + result = Address.verifyCallResult( + success, + returnData, + "ERC725X: Unknown Error" + ); + } + + /** + * @dev perform low-level delegatecall (operation type = 4) + * @param target The address on which delegatecall is executed + * @param data The data to be sent with the delegatecall + * @return result The data returned from the delegatecall + */ + function _executeDelegateCall(address target, bytes memory data) + internal + virtual + returns (bytes memory result) + { + emit Executed(OPERATION_4_DELEGATECALL, target, 0, bytes4(data)); + + // solhint-disable avoid-low-level-calls + (bool success, bytes memory returnData) = target.delegatecall(data); + result = Address.verifyCallResult( + success, + returnData, + "ERC725X: Unknown Error" + ); + } + + /** + * @dev deploy a contract using the CREATE opcode (operation type = 1) + * @param value The value to be sent to the contract created + * @param creationCode The contract creation bytecode to deploy appended with the constructor argument(s) + * @return newContract The address of the contract created as bytes + */ + function _deployCreate(uint256 value, bytes memory creationCode) + internal + virtual + returns (bytes memory newContract) + { + if (creationCode.length == 0) { + revert ERC725X_NoContractBytecodeProvided(); + } + + address contractAddress; + // solhint-disable no-inline-assembly + assembly { + contractAddress := create( + value, + add(creationCode, 0x20), + mload(creationCode) + ) + } + + if (contractAddress == address(0)) { + revert ERC725X_ContractDeploymentFailed(); + } + + newContract = abi.encodePacked(contractAddress); + emit ContractCreated( + OPERATION_1_CREATE, + contractAddress, + value, + bytes32(0) + ); + } + + /** + * @dev deploy a contract using the CREATE2 opcode (operation type = 2) + * @param value The value to be sent to the contract created + * @param creationCode The contract creation bytecode to deploy appended with the constructor argument(s) and a bytes32 salt + * @return newContract The address of the contract created as bytes + */ + function _deployCreate2(uint256 value, bytes memory creationCode) + internal + virtual + returns (bytes memory newContract) + { + bytes32 salt = BytesLib.toBytes32( + creationCode, + creationCode.length - 32 + ); + bytes memory bytecode = BytesLib.slice( + creationCode, + 0, + creationCode.length - 32 + ); + + address contractAddress; + require( + address(this).balance >= value, + "Create2: insufficient balance" + ); + require(creationCode.length != 0, "Create2: bytecode length is zero"); + /// @solidity memory-safe-assembly + assembly { + contractAddress := create2( + value, + add(bytecode, 0x20), + mload(bytecode), + salt + ) + } + require(contractAddress != address(0), "Create2: Failed on deploy"); + + newContract = abi.encodePacked(contractAddress); + emit ContractCreated(OPERATION_2_CREATE2, contractAddress, value, salt); + } +} + +/** + * @title The interface for ERC725Y General data key/value store + * @dev ERC725Y provides the ability to set arbitrary data key/value pairs that can be changed over time + * It is intended to standardise certain data key/value pairs to allow automated read and writes + * from/to the contract storage + */ +interface IERC725Y is IERC165 { + /** + * @notice Emitted when data at a key is changed + * @param dataKey The data key which data value is set + * @param dataValue The data value to set + */ + event DataChanged(bytes32 indexed dataKey, bytes dataValue); + + /** + * @notice Gets singular data at a given `dataKey` + * @param dataKey The key which value to retrieve + * @return dataValue The data stored at the key + */ + function getData(bytes32 dataKey) + external + view + returns (bytes memory dataValue); + + /** + * @notice Gets array of data for multiple given keys + * @param dataKeys The array of keys which values to retrieve + * @return dataValues The array of data stored at multiple keys + */ + function getData(bytes32[] memory dataKeys) + external + view + returns (bytes[] memory dataValues); + + /** + * @notice Sets singular data for a given `dataKey` + * @param dataKey The key to retrieve stored value + * @param dataValue The value to set + * SHOULD only be callable by the owner of the contract set via ERC173 + * + * Emits a {DataChanged} event. + */ + function setData(bytes32 dataKey, bytes memory dataValue) external; + + /** + * @param dataKeys The array of data keys for values to set + * @param dataValues The array of values to set + * @dev Sets array of data for multiple given `dataKeys` + * SHOULD only be callable by the owner of the contract set via ERC173 + * + * Emits a {DataChanged} event. + */ + function setData(bytes32[] memory dataKeys, bytes[] memory dataValues) + external; +} + +// ERC165 INTERFACE IDs +bytes4 constant _INTERFACEID_ERC725Y = 0x714df77c; + +/** + * @dev reverts when there is not the same number of elements in the lists of data keys and data values + * when calling setData(bytes32[],bytes[]). + * @param dataKeysLength the number of data keys in the bytes32[] dataKeys + * @param dataValuesLength the number of data value in the bytes[] dataValue + */ +error ERC725Y_DataKeysValuesLengthMismatch( + uint256 dataKeysLength, + uint256 dataValuesLength +); + +contract ERC725Y is ERC173, ERC165, IERC725Y { + // overrides + + /** + * @inheritdoc ERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(IERC165, ERC165) + returns (bool) + { + return + interfaceId == _INTERFACEID_ERC725Y || + super.supportsInterface(interfaceId); + } + + /** + * @dev Map the dataKeys to their dataValues + */ + mapping(bytes32 => bytes) internal _store; + + /** + * @inheritdoc IERC725Y + */ + function getData(bytes32 dataKey) + public + view + virtual + returns (bytes memory dataValue) + { + dataValue = _getData(dataKey); + } + + /** + * @inheritdoc IERC725Y + */ + function getData(bytes32[] memory dataKeys) + public + view + virtual + returns (bytes[] memory dataValues) + { + dataValues = new bytes[](dataKeys.length); + + for (uint256 i = 0; i < dataKeys.length; i++) { + dataValues[i] = _getData(dataKeys[i]); + } + + return dataValues; + } + + /** + * @inheritdoc IERC725Y + */ + function setData(bytes32 dataKey, bytes memory dataValue) + public + virtual + onlyOwner + { + _setData(dataKey, dataValue); + } + + /** + * @inheritdoc IERC725Y + */ + function setData(bytes32[] memory dataKeys, bytes[] memory dataValues) + public + virtual + onlyOwner + { + if (dataKeys.length != dataValues.length) { + revert ERC725Y_DataKeysValuesLengthMismatch( + dataKeys.length, + dataValues.length + ); + } + + for (uint256 i = 0; i < dataKeys.length; i++) { + _setData(dataKeys[i], dataValues[i]); + } + } + + function _getData(bytes32 dataKey) + internal + view + virtual + returns (bytes memory dataValue) + { + return _store[dataKey]; + } + + function _setData(bytes32 dataKey, bytes memory dataValue) + internal + virtual + { + _store[dataKey] = dataValue; + emit DataChanged(dataKey, dataValue); + } +} + +contract ERC725 is ERC725X, ERC725Y { + /** + * @inheritdoc ERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC725X, ERC725Y) + returns (bool) + { + return + interfaceId == _INTERFACEID_ERC725X || + interfaceId == _INTERFACEID_ERC725Y || + super.supportsInterface(interfaceId); + } +} + +// external needed libraries + +library BytesLib { + function concat(bytes memory _preBytes, bytes memory _postBytes) + internal + pure + returns (bytes memory) + { + bytes memory tempBytes; + + assembly { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // Store the length of the first bytes array at the beginning of + // the memory for tempBytes. + let length := mload(_preBytes) + mstore(tempBytes, length) + + // Maintain a memory counter for the current write location in the + // temp bytes array by adding the 32 bytes for the array length to + // the starting location. + let mc := add(tempBytes, 0x20) + // Stop copying when the memory counter reaches the length of the + // first bytes array. + let end := add(mc, length) + + for { + // Initialize a copy counter to the start of the _preBytes data, + // 32 bytes into its memory. + let cc := add(_preBytes, 0x20) + } lt(mc, end) { + // Increase both counters by 32 bytes each iteration. + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // Write the _preBytes data into the tempBytes memory 32 bytes + // at a time. + mstore(mc, mload(cc)) + } + + // Add the length of _postBytes to the current length of tempBytes + // and store it as the new length in the first 32 bytes of the + // tempBytes memory. + length := mload(_postBytes) + mstore(tempBytes, add(length, mload(tempBytes))) + + // Move the memory counter back from a multiple of 0x20 to the + // actual end of the _preBytes data. + mc := end + // Stop copying when the memory counter reaches the new combined + // length of the arrays. + end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + // Update the free-memory pointer by padding our last write location + // to 32 bytes: add 31 bytes to the end of tempBytes to move to the + // next 32 byte block, then round down to the nearest multiple of + // 32. If the sum of the length of the two arrays is zero then add + // one before rounding down to leave a blank 32 bytes (the length block with 0). + mstore( + 0x40, + and( + add(add(end, iszero(add(length, mload(_preBytes)))), 31), + not(31) // Round down to the nearest 32 bytes. + ) + ) + } + + return tempBytes; + } + + function concatStorage(bytes storage _preBytes, bytes memory _postBytes) + internal + { + assembly { + // Read the first 32 bytes of _preBytes storage, which is the length + // of the array. (We don't need to use the offset into the slot + // because arrays use the entire slot.) + let fslot := sload(_preBytes.slot) + // Arrays of 31 bytes or less have an even value in their slot, + // while longer arrays have an odd value. The actual length is + // the slot divided by two for odd values, and the lowest order + // byte divided by two for even values. + // If the slot is even, bitwise and the slot with 255 and divide by + // two to get the length. If the slot is odd, bitwise and the slot + // with -1 and divide by two. + let slength := div( + and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), + 2 + ) + let mlength := mload(_postBytes) + let newlength := add(slength, mlength) + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + switch add(lt(slength, 32), lt(newlength, 32)) + case 2 { + // Since the new array still fits in the slot, we just need to + // update the contents of the slot. + // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length + sstore( + _preBytes.slot, + // all the modifications to the slot are inside this + // next block + add( + // we can just add to the slot contents because the + // bytes we want to change are the LSBs + fslot, + add( + mul( + div( + // load the bytes from memory + mload(add(_postBytes, 0x20)), + // zero all bytes to the right + exp(0x100, sub(32, mlength)) + ), + // and now shift left the number of bytes to + // leave space for the length in the slot + exp(0x100, sub(32, newlength)) + ), + // increase length by the double of the memory + // bytes length + mul(mlength, 2) + ) + ) + ) + } + case 1 { + // The stored value fits in the slot, but the combined value + // will exceed it. + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // The contents of the _postBytes array start 32 bytes into + // the structure. Our first read should obtain the `submod` + // bytes that can fit into the unused space in the last word + // of the stored array. To get this, we read 32 bytes starting + // from `submod`, so the data we read overlaps with the array + // contents by `submod` bytes. Masking the lowest-order + // `submod` bytes allows us to add that value directly to the + // stored value. + + let submod := sub(32, slength) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore( + sc, + add( + and( + fslot, + 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00 + ), + and(mload(mc), mask) + ) + ) + + for { + mc := add(mc, 0x20) + sc := add(sc, 1) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + default { + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + // Start copying to the last used word of the stored array. + let sc := add(keccak256(0x0, 0x20), div(slength, 32)) + + // save new length + sstore(_preBytes.slot, add(mul(newlength, 2), 1)) + + // Copy over the first `submod` bytes of the new data as in + // case 1 above. + let slengthmod := mod(slength, 32) + let mlengthmod := mod(mlength, 32) + let submod := sub(32, slengthmod) + let mc := add(_postBytes, submod) + let end := add(_postBytes, mlength) + let mask := sub(exp(0x100, submod), 1) + + sstore(sc, add(sload(sc), and(mload(mc), mask))) + + for { + sc := add(sc, 1) + mc := add(mc, 0x20) + } lt(mc, end) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + sstore(sc, mload(mc)) + } + + mask := exp(0x100, sub(mc, end)) + + sstore(sc, mul(div(mload(mc), mask), mask)) + } + } + } + + function slice( + bytes memory _bytes, + uint256 _start, + uint256 _length + ) internal pure returns (bytes memory) { + require(_length + 31 >= _length, "slice_overflow"); + require(_bytes.length >= _start + _length, "slice_outOfBounds"); + + bytes memory tempBytes; + + assembly { + switch iszero(_length) + case 0 { + // Get a location of some free memory and store it in tempBytes as + // Solidity does for memory variables. + tempBytes := mload(0x40) + + // The first word of the slice result is potentially a partial + // word read from the original array. To read it, we calculate + // the length of that partial word and start copying that many + // bytes into the array. The first word we copy will start with + // data we don't care about, but the last `lengthmod` bytes will + // land at the beginning of the contents of the new array. When + // we're done copying, we overwrite the full first word with + // the actual length of the slice. + let lengthmod := and(_length, 31) + + // The multiplication in the next line is necessary + // because when slicing multiples of 32 bytes (lengthmod == 0) + // the following copy loop was copying the origin's length + // and then ending prematurely not copying everything it should. + let mc := add( + add(tempBytes, lengthmod), + mul(0x20, iszero(lengthmod)) + ) + let end := add(mc, _length) + + for { + // The multiplication in the next line has the same exact purpose + // as the one above. + let cc := add( + add( + add(_bytes, lengthmod), + mul(0x20, iszero(lengthmod)) + ), + _start + ) + } lt(mc, end) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + mstore(mc, mload(cc)) + } + + mstore(tempBytes, _length) + + //update free-memory pointer + //allocating the array padded to 32 bytes like the compiler does now + mstore(0x40, and(add(mc, 31), not(31))) + } + //if we want a zero-length slice let's just return a zero-length array + default { + tempBytes := mload(0x40) + //zero out the 32 bytes slice we are about to return + //we need to do it because Solidity does not garbage collect + mstore(tempBytes, 0) + + mstore(0x40, add(tempBytes, 0x20)) + } + } + + return tempBytes; + } + + function toAddress(bytes memory _bytes, uint256 _start) + internal + pure + returns (address) + { + require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); + address tempAddress; + + assembly { + tempAddress := div( + mload(add(add(_bytes, 0x20), _start)), + 0x1000000000000000000000000 + ) + } + + return tempAddress; + } + + function toUint8(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint8) + { + require(_bytes.length >= _start + 1, "toUint8_outOfBounds"); + uint8 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x1), _start)) + } + + return tempUint; + } + + function toUint16(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint16) + { + require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); + uint16 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x2), _start)) + } + + return tempUint; + } + + function toUint32(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint32) + { + require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); + uint32 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x4), _start)) + } + + return tempUint; + } + + function toUint64(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint64) + { + require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); + uint64 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x8), _start)) + } + + return tempUint; + } + + function toUint96(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint96) + { + require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); + uint96 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0xc), _start)) + } + + return tempUint; + } + + function toUint128(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint128) + { + require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); + uint128 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x10), _start)) + } + + return tempUint; + } + + function toUint256(bytes memory _bytes, uint256 _start) + internal + pure + returns (uint256) + { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } + + return tempUint; + } + + function toBytes32(bytes memory _bytes, uint256 _start) + internal + pure + returns (bytes32) + { + require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); + bytes32 tempBytes32; + + assembly { + tempBytes32 := mload(add(add(_bytes, 0x20), _start)) + } + + return tempBytes32; + } + + function equal(bytes memory _preBytes, bytes memory _postBytes) + internal + pure + returns (bool) + { + bool success = true; + + assembly { + let length := mload(_preBytes) + + // if lengths don't match the arrays are not equal + switch eq(length, mload(_postBytes)) + case 1 { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + let mc := add(_preBytes, 0x20) + let end := add(mc, length) + + for { + let cc := add(_postBytes, 0x20) + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + } eq(add(lt(mc, end), cb), 2) { + mc := add(mc, 0x20) + cc := add(cc, 0x20) + } { + // if any of these checks fails then arrays are not equal + if iszero(eq(mload(mc), mload(cc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } + + function equalStorage(bytes storage _preBytes, bytes memory _postBytes) + internal + view + returns (bool) + { + bool success = true; + + assembly { + // we know _preBytes_offset is 0 + let fslot := sload(_preBytes.slot) + // Decode the length of the stored array like in concatStorage(). + let slength := div( + and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), + 2 + ) + let mlength := mload(_postBytes) + + // if lengths don't match the arrays are not equal + switch eq(slength, mlength) + case 1 { + // slength can contain both the length and contents of the array + // if length < 32 bytes so let's prepare for that + // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage + if iszero(iszero(slength)) { + switch lt(slength, 32) + case 1 { + // blank the last byte which is the length + fslot := mul(div(fslot, 0x100), 0x100) + + if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { + // unsuccess: + success := 0 + } + } + default { + // cb is a circuit breaker in the for loop since there's + // no said feature for inline assembly loops + // cb = 1 - don't breaker + // cb = 0 - break + let cb := 1 + + // get the keccak hash to get the contents of the array + mstore(0x0, _preBytes.slot) + let sc := keccak256(0x0, 0x20) + + let mc := add(_postBytes, 0x20) + let end := add(mc, mlength) + + // the next line is the loop condition: + // while(uint256(mc < end) + cb == 2) + for { + + } eq(add(lt(mc, end), cb), 2) { + sc := add(sc, 1) + mc := add(mc, 0x20) + } { + if iszero(eq(sload(sc), mload(mc))) { + // unsuccess: + success := 0 + cb := 0 + } + } + } + } + } + default { + // unsuccess: + success := 0 + } + } + + return success; + } +} + +library Address { + /** + * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the + * revert reason using the provided one. + */ + function verifyCallResult( + bool success, + bytes memory returndata, + string memory errorMessage + ) internal pure returns (bytes memory) { + if (success) { + return returndata; + } else { + // Look for revert reason and bubble it up if present + if (returndata.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(returndata) + revert(add(32, returndata), returndata_size) + } + } else { + revert(errorMessage); + } + } + } +} From 03cc2f375bc920429d402c5734bd99c24a4a5558 Mon Sep 17 00:00:00 2001 From: Antonio Viggiano Date: Tue, 3 Jan 2023 11:44:10 -0300 Subject: [PATCH 104/274] Center layout (#6231) --- assets/css/style.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/css/style.scss b/assets/css/style.scss index 717e3cd4b983ac..125b56640af918 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -25,6 +25,10 @@ } main.page-content { + max-width: 960px; + display: flex; + align-self: center; + div.wrapper { max-width: unset; } From 043ba73ee6c4a1175e68c3d77357bbaa5d9a0251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Tue, 3 Jan 2023 16:35:04 +0100 Subject: [PATCH 105/274] EIP-5450: Revision 2 (#5993) * EIP-5450: Revision 2 * Change steps structure * EIP-5450: Add definition of terminating instruction * EIP-5450: Avoid vague "required stack height" term * Fix typos Co-authored-by: Jochem Brouwer Co-authored-by: Jochem Brouwer --- EIPS/eip-5450.md | 93 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 5a6c369b575a4f..9e47d3617231e3 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -13,41 +13,87 @@ requires: 3540, 3670, 4200, 4750 ## Abstract -Introduce extended validation of code sections to guarantee that neither stack underflow nor overflow can happen during execution of validated contracts. +Introduce extended validation of EOF code sections to guarantee that neither stack underflow nor overflow can happen during execution of validated contracts. ## Motivation -Currently existing EVM implementations perform a number of validity checks for each executed instruction, such as check for stack overflow/underflow, sufficient gas, etc. This change aims to minimize the number of such checks required at run-time, by verifying at deploy-time that no exceptional conditions can happen, and preventing to deploy the code where it could happen. +The current EVM performs a number of validity checks for each executed instruction, such as checking +for instruction being defined, stack overflow and underflow, and enough amount of gas remaining. -In particular, this extended code validation eliminates the need for EVM stack underflow checks done for every executed instruction. It also prevents deploying code that can be statically proven to require more than 1024 stack items and limits stack overflow checks to the `CALLF` instruction only. +This EIP minimizes the number of such checks required at run-time +by verifying that no exceptional conditions can happen +and preventing the execution and deployment of any invalid code. + +The operand stack validation provides several benefits: + +- removes run-time stack underflow check for all instructions, +- removes run-time stack overflow check for all instruction except `CALLF`, +- ensures that an execution terminates with one of the terminating instructions, +- prevents the deployment of code with unreachable instructions, which discourages the use of code sections for data storage. + +It also has some disadvantages: + +- adds constraints to the code structure (similar to JVM, CPython bytecode, WebAssembly and others); however, these constraints can be lifted in a backward-compatible manner if they are shown to be user-unfriendly, +- adds second validation pass; however, validation's computational and space complexity remains linear. ## Specification ### Code validation -*Remark:* We rely on the notions of *data stack* and *type section* as defined by [EIP-4750](./eip-4750.md). +*Remark:* We rely on the notions of *operand stack* (FIXME) and *type section* as defined by [EIP-4750](./eip-4750.md). + +Each code section is validated independently. + +#### Instructions validation + +In the first validation phase defined in [EIP-3670](./eip-3670.md) (and extended by [EIP-4200](./eip-4200.md) and [EIP-4750](./eip-4750.md)) instructions are inspected independently to check if their opcodes and immediate values are valid. + +#### Operand stack validation + +In the second validation phase control-flow analysis is performed on the code. -Code section validation rules as defined by [EIP-3670](./eip-3670.md) (which has been extended by [EIP-4200](./eip-4200.md) and [EIP-4750](./eip-4750.md)) are extended again to include the instruction flow traversal procedure, where every possible code path is examined, and data stack height at each instruction is recorded. +*Operand stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual data stack implementation, but only to keep track of its height. -*Data stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual data stack implementation, but only to keep track of its height. Current height value starts at `type[code_section_index].inputs` (number of inputs of this function) at function entry and is updated at each instruction. +*Terminating instructions* refers to the instructions either: -At the same time the following properties are being verified: +- ending function execution: `RETF`, or +- ending whole EVM execution: `STOP`, `RETURN`, `REVERT`, `INVALID`. -1. For each reachable instruction in the section, data stack height is the same for all possible code paths going through this instruction. -2. For each instruction, data stack always has enough items, i.e. stack underflow is invalid. -3. For `CALLF` instruction, data stack has enough items to use as input arguments to a called function according to its type defined in the type section. -4. For `RETF` instruction, data stack before executing it has exactly `n` items to use as output values returned from a function, where `n` is function's number of outputs according to its type defined in the type section. -5. `max_stack_height` does not exceed 1023. -6. The maximum data stack height matches the corresponding code section's `max_stack_height` within the type section body. -7. No unreachable instructions exist in the code section. +For each reachable instruction in the code the operand stack height is recorded. +The first instruction has the recorded stack height equal to the number of inputs to the function type matching the code (`type[code_section_index].inputs`). -To examine every reachable code path, validation needs to traverse every instruction in order, while also following each non-conditional jump, and following both possible branches for each conditional jump. After completing this, verify each instruction was visited at least once. Fail if any instructions were not visited as this invalidates 7). +The FIFO queue *worklist* is used for tracking "to be inspected" instructions. Initially, it contains the first instruction. -The complexity of this traversal is linear in the number of instructions, because each code path is examined only once, and property 1 guarantees no loops in the validation. +While *worklist* is not empty, dequeue an instruction and: + +1. Determine the effect the instruction has on the operand stack: + 1. **Check** if the recorded stack height satisfies the instruction requirements. Specifically: + - for `CALLF` instruction the recorded stack height must be at least the number of inputs of the called function according to its type defined in the type section. + - for `RETF` instruction the recorded stack height must be exactly the number of outputs of the function matching the code. + 2. Compute new stack height after the instruction execution. +2. Determine the list of successor instructions that can follow the current instructions: + 1. The next instruction for all instructions other than terminating instructions and unconditional jump. + 2. All targets of a conditional or unconditional jump. +3. For each successor instruction: + 1. **Check** if the instruction is present in the code (i.e. execution must not "fall off" the code). + 2. If the instruction does not have stack height recorded (visited for the first time): + 1. Record the instruction stack height as the value computed in 1.2. + 2. Add the instruction to the *worklist*. + 3. Otherwise, **check** if the recorded stack height equals the value computed in 1.2. + +When *worklist* is empty: + +1. **Check** if all instruction were reached by the analysis. +2. Determine the function maximum operand stack height: + 1. Compute the maximum stack height as the maximum of all recorded stack heights. + 2. **Check** if the maximum stack height does not exceed the limit of 1023 (`0x3FF`). + 3. **Check** if the maximum stack height matches the corresponding code section's `max_stack_height` within the type section. + +The computational and space complexity of this pass is *O(len(code))*. Each instruction is visited at most once. ### Execution -Given new deploy-time guarantees, EVM implementation is not required anymore to have run-time stack underflow nor overflow checks for each executed instruction. The exception is the `CALLF` performing data stack overflow check for the entire called function. +Given the deploy-time validation guarantees, an EVM implementation is not required anymore to have run-time stack underflow nor overflow checks for each executed instruction. The exception is the `CALLF` performing data stack overflow check for the entire called function. ## Rationale @@ -55,9 +101,20 @@ Given new deploy-time guarantees, EVM implementation is not required anymore to In this EIP, we provide a more efficient variant of the EVM where stack overflow check is performed only in `CALLF` instruction using the called function's `max_stack_height` information. This decreases flexibility of an EVM program because `max_stack_height` corresponds to the worst-case control-flow path in the function. +### Unreachable code + +The operand stack validation algorithm rejects any code having any unreachable instructions. This check can be performed very cheaply. It prevents deploying degenerated code. Moreover, it enables combining instruction validation and operand stack validation into single pass. + +### Clean stack upon termination + +It is currently required that the operand stack is empty (in the current function context) after the `RETF` instruction. +Otherwise, the `RETF` semantic would be more complicated. For `n` function outputs and `s` the stack height at `RETF` the EVM must erase `s-n` non-top stack items and move the `n` stack items to the place of erased ones. Cost of such operation may be relatively cheap but is not constant. +However, lifting the requirement and modifying the `RETF` semantic as described above is backward +compatible and can be easily introduced in the future. + ## Backwards Compatibility -This change requires a “network upgrade”, since it modifies consensus rules. +This change requires a "network upgrade", since it modifies consensus rules. It poses no risk to backwards compatibility, as it is introduced only for EOF1 contracts, for which deploying undefined instructions is not allowed, therefore there are no existing contracts using these instructions. The new instructions are not introduced for legacy bytecode (code which is not EOF formatted). From b81f1f5d18e9a7b695b148b1774ba968b70cf70c Mon Sep 17 00:00:00 2001 From: William Morriss Date: Tue, 3 Jan 2023 10:08:38 -0600 Subject: [PATCH 106/274] Revert #6009 and satisfy updated lint (#6226) * Revert "(bot 1272989785) moving EIPS/eip-3651.md to stagnant (#6009)" This reverts commit 2d2e28b8bacd2b069897b38183a05a0dc6223441. * lint * linkify eip-20 * consistent pronouns --- EIPS/eip-3651.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-3651.md b/EIPS/eip-3651.md index 96688016921f23..67666a415871b2 100644 --- a/EIPS/eip-3651.md +++ b/EIPS/eip-3651.md @@ -4,7 +4,7 @@ title: Warm COINBASE description: Starts the `COINBASE` address warm author: William Morriss (@wjmelements) discussions-to: https://ethereum-magicians.org/t/eip-3651-warm-coinbase/6640 -status: Stagnant +status: Review type: Standards Track category: Core created: 2021-07-12 @@ -12,27 +12,34 @@ requires: 2929 --- ## Abstract + The `COINBASE` address shall be warm at the start of transaction execution, in accordance with the actual cost of reading that account. ## Motivation + Direct `COINBASE` payments are becoming increasingly popular because they allow conditional payments, which provide benefits such as implicit cancellation of transactions that would revert. But accessing `COINBASE` is overpriced; the address is initially cold under the access list framework introduced in [EIP-2929](./eip-2929.md). -This gas cost mismatch can incentivize alternative payments besides ETH, such as ERC20, but ETH should be the primary means of paying for transactions on Ethereum. +This gas cost mismatch can incentivize alternative payments besides ETH, such as [EIP-20](./eip-20.md), but ETH should be the primary means of paying for transactions on Ethereum. ## Specification + At the start of transaction execution, `accessed_addresses` shall be initialized to also include the address returned by `COINBASE` (`0x41`). ## Rationale + The addresses currently initialized warm are the addresses that should already be loaded at the start of transaction validation. The `ORIGIN` address is always loaded to check its balance against the gas limit and the gas price. The `tx.to` address is always loaded to begin execution. -The `COINBASE` address should also be always be loaded because they receive the block reward as well as the transaction fees. +The `COINBASE` address should also be always be loaded because it receives the block reward and the transaction fees. ## Backwards Compatibility + There are no known backward compatibility issues presented by this change. ## Security Considerations + There are no known security considerations introduced by this change. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 112571f73ebe6bd4c1e8c6eaeea979d8b92a257f Mon Sep 17 00:00:00 2001 From: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Date: Tue, 3 Jan 2023 12:02:04 -0500 Subject: [PATCH 107/274] Attempt to fix the page width issue (#6254) --- assets/css/style.scss | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/assets/css/style.scss b/assets/css/style.scss index 125b56640af918..2624f9b84cb5d2 100644 --- a/assets/css/style.scss +++ b/assets/css/style.scss @@ -2,6 +2,8 @@ # Only the main Sass file needs front matter (the dashes are enough) --- +$content-width: 960px; + @import 'minima'; .site-header { @@ -24,16 +26,6 @@ } } -main.page-content { - max-width: 960px; - display: flex; - align-self: center; - - div.wrapper { - max-width: unset; - } -} - .footer-col-wrapper { color: #111; } From 9fd46dd724d9528bb98164625779fdefb607ff23 Mon Sep 17 00:00:00 2001 From: yaruno Date: Tue, 3 Jan 2023 18:31:41 +0100 Subject: [PATCH 108/274] Update from last call to final (#6244) --- EIPS/eip-5023.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-5023.md b/EIPS/eip-5023.md index 50db6e7d30ef89..c3e36b57e2b83c 100644 --- a/EIPS/eip-5023.md +++ b/EIPS/eip-5023.md @@ -4,8 +4,7 @@ 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: Last Call -last-call-deadline: 2022-12-31 +status: Final type: Standards Track category: ERC created: 2022-01-28 From ce9748cc0881632974943ad55f67518dde8a8de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Wed, 4 Jan 2023 13:15:05 +0100 Subject: [PATCH 109/274] 3540,4750,5450: Rename data stack to operand stack (#6253) --- EIPS/eip-3540.md | 16 ++++++++-------- EIPS/eip-4750.md | 24 ++++++++++++------------ EIPS/eip-5450.md | 6 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index 89aa8a109cc51c..a9b51afac6c92e 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -133,14 +133,14 @@ type_section := (inputs, outputs, max_stack_height)+ ##### Body -| name | length | value | description | -|------------------|----------|--------------|-------------| -| type_section | variable | n/a | stores EIP-4750 and EIP-5450 code section metadata | -| inputs | 1 byte | 0x00-0x7F | number of stack elements the code section consumes | -| outputs | 1 byte | 0x00-0x7F | number of stack elements the code section returns | -| max_stack_height | 2 bytes | 0x0000-0x3FF | max height of data stack during execution | -| code_section | variable | n/a | arbitrary bytecode | -| data_section | variable | n/a | arbitrary sequence of bytes | +| name | length | value | description | +|------------------|----------|--------------|----------------------------------------------------| +| type_section | variable | n/a | stores EIP-4750 and EIP-5450 code section metadata | +| inputs | 1 byte | 0x00-0x7F | number of stack elements the code section consumes | +| outputs | 1 byte | 0x00-0x7F | number of stack elements the code section returns | +| max_stack_height | 2 bytes | 0x0000-0x3FF | max height of operand stack during execution | +| code_section | variable | n/a | arbitrary bytecode | +| data_section | variable | n/a | arbitrary sequence of bytes | See [EIP-4750](./eip-4750.md) for more information on the type section content. diff --git a/EIPS/eip-4750.md b/EIPS/eip-4750.md index 87746d15aeb59c..8dded5a807b6c1 100644 --- a/EIPS/eip-4750.md +++ b/EIPS/eip-4750.md @@ -39,7 +39,7 @@ Refer to [EIP-3540](./eip-3540.md) to see the full structure of a well-formed EO ### New execution state in EVM -A return stack is introduced, separate from the data stack. It is a stack of items representing execution state to return to after function execution is finished. Each item is comprised of: code section index, offset in the code section (PC value), calling function stack height. +A return stack is introduced, separate from the operand stack. It is a stack of items representing execution state to return to after function execution is finished. Each item is comprised of: code section index, offset in the code section (PC value), calling function stack height. Note: Implementations are free to choose particular encoding for a stack item. In the specification below we assume that representation is three unsigned integers: `code_section_index`, `offset`, `stack_height`. @@ -67,11 +67,11 @@ If the code is valid EOF1, the following execution rules apply: #### `CALLF` 1. Has one immediate argument,`code_section_index`, encoded as a 16-bit unsigned big-endian value. -2. If data stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. -3. If data stack size exceeds `1024 - type[code_section_index].max_stack_height` (i.e. if the called function may exceed the global stack height limit), execution results in exceptional halt. This also guarantees that the stack height after the call is within the limits. +2. If operand stack has less than `caller_stack_height + type[code_section_index].inputs` items, execution results in exceptional halt. +3. If operand stack size exceeds `1024 - type[code_section_index].max_stack_height` (i.e. if the called function may exceed the global stack height limit), execution results in exceptional halt. This also guarantees that the stack height after the call is within the limits. 4. If return stack already has `1024` items, execution results in exceptional halt. 5. Charges 5 gas. -6. Pops nothing and pushes nothing to data stack. +6. Pops nothing and pushes nothing to operand stack. 7. Pushes to return stack an item: ``` @@ -80,7 +80,7 @@ If the code is valid EOF1, the following execution rules apply: stack_height = data_stack.height - types[code_section_index].inputs) ``` - Under `PC_post_instruction` we mean the PC position after the entire immediate argument of `CALLF`. Data stack height is saved as it was before function inputs were pushed. + Under `PC_post_instruction` we mean the PC position after the entire immediate argument of `CALLF`. Operand stack height is saved as it was before function inputs were pushed. *Note:* Code validation rules of [EIP-5450](./eip-5450.md) guarantee there is always an instruction following `CALLF` (since terminating instruction or unconditional jump is required to be final one in the section), therefore `PC_post_instruction` always points to an instruction inside section bounds. 8. Sets `current_section_index` to `code_section_index` and `PC` to `0`, and execution continues in the called section. @@ -88,10 +88,10 @@ If the code is valid EOF1, the following execution rules apply: #### `RETF` 1. Does not have immediate arguments. -2. If data stack does not equal `caller_stack_height + type[current_section_index].outputs` items, execution results in exceptional halt. -4. Charges 3 gas. -5. Pops nothing and pushes nothing to data stack. -6. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. +2. If operand stack does not equal `caller_stack_height + type[current_section_index].outputs` items, execution results in exceptional halt. +3. Charges 3 gas. +4. Pops nothing and pushes nothing to operand stack. +5. Pops an item from return stack and sets `current_section_index` and `PC` to values from this item. 1. If return stack is empty after this, execution halts with success. ### Code Validation @@ -108,7 +108,7 @@ In addition to container format validation rules above, we extend code section v Dynamic jump instructions `JUMP` (`0x56`) and `JUMPI` (`0x57`) are invalid and their opcodes are undefined. -`JUMPDEST` (`0x5b`) instruction is renamed to `NOP` ("no operation") without the change in behaviour: it pops nothing and pushes nothing to data stack and has no other effects except for `PC` increment and charging 1 gas. +`JUMPDEST` (`0x5b`) instruction is renamed to `NOP` ("no operation") without the change in behaviour: it pops nothing and pushes nothing to operand stack and has no other effects except for `PC` increment and charging 1 gas. `PC` (0x58) instruction becomes invalid and its opcode is undefined. @@ -118,8 +118,8 @@ Dynamic jump instructions `JUMP` (`0x56`) and `JUMPI` (`0x57`) are invalid and t 1. Execution starts at the first byte of the first code section, and PC is set to 0. 2. Return stack is initialized to contain one item: `(code_section_index = 0, offset = 0, stack_height = 0)` -3. If any instruction would access a data stack item below `caller_stack_height`, execution results in exceptional halt. This rule replaces the old stack underflow check. -4. No change in stack overflow check: if any instruction would result in data stack size exceeding `1024` items, execution results in exceptional halt. +3. If any instruction access the operand stack item below `caller_stack_height`, execution results in exceptional halt. This rule replaces the old stack underflow check. +4. No change in stack overflow check: if any instruction causes the operand stack height to exceed `1024`, execution results in exceptional halt. ## Rationale diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 9e47d3617231e3..1a7fb49a9448f6 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -40,7 +40,7 @@ It also has some disadvantages: ### Code validation -*Remark:* We rely on the notions of *operand stack* (FIXME) and *type section* as defined by [EIP-4750](./eip-4750.md). +*Remark:* We rely on the notions of *operand stack* and *type section* as defined by [EIP-4750](./eip-4750.md). Each code section is validated independently. @@ -52,7 +52,7 @@ In the first validation phase defined in [EIP-3670](./eip-3670.md) (and extended In the second validation phase control-flow analysis is performed on the code. -*Operand stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual data stack implementation, but only to keep track of its height. +*Operand stack height* here refers to the number of stack values accessible by this function, i.e. it does not take into account values of caller functions' frames (but does include this function's inputs). Note that validation procedure does not require actual operand stack implementation, but only to keep track of its height. *Terminating instructions* refers to the instructions either: @@ -93,7 +93,7 @@ The computational and space complexity of this pass is *O(len(code))*. Each inst ### Execution -Given the deploy-time validation guarantees, an EVM implementation is not required anymore to have run-time stack underflow nor overflow checks for each executed instruction. The exception is the `CALLF` performing data stack overflow check for the entire called function. +Given the deploy-time validation guarantees, an EVM implementation is not required anymore to have run-time stack underflow nor overflow checks for each executed instruction. The exception is the `CALLF` performing operand stack overflow check for the entire called function. ## Rationale From e049d8020e2c36629dc57a5f02554176dc30d7d1 Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Wed, 4 Jan 2023 23:18:32 +0100 Subject: [PATCH 110/274] Upgrade EIP-6059 to Review status (#6221) As the EIP-6059 is ready for peer review, we are upgrading it to `Review` status to request it. --- EIPS/eip-6059.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6059.md b/EIPS/eip-6059.md index ebafebe5ef4fd2..216cea55f0f2d9 100644 --- a/EIPS/eip-6059.md +++ b/EIPS/eip-6059.md @@ -4,7 +4,7 @@ title: Parent-Governed Nestable Non-Fungible Tokens description: An interface for Nestable Non-Fungible Tokens with emphasis on parent token's control over the relationship. author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) discussions-to: https://ethereum-magicians.org/t/eip-6059-parent-governed-nestable-non-fungible-tokens/11914 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-11-15 From ff0c652dc6c5b28951ceba405856dfe5b263e31e Mon Sep 17 00:00:00 2001 From: Suning Yao Date: Wed, 4 Jan 2023 17:23:11 -0500 Subject: [PATCH 111/274] Update EIP-6150: Move to Review (#6219) * Update EIP-6150: Move to Review * Update EIP-6150: Fix wording * Update EIP-6150: Fix typo * Update EIP-6150: Add ERC-165 identifiers --- EIPS/eip-6150.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-6150.md b/EIPS/eip-6150.md index 3c065319ed5bcf..279ea7ba531057 100644 --- a/EIPS/eip-6150.md +++ b/EIPS/eip-6150.md @@ -4,7 +4,7 @@ title: Hierarchical NFTs description: Hierarchical NFTs, an extension to EIP-721. author: Keegan Lee (@keeganlee), msfew , Kartin , qizhou (@qizhou) discussions-to: https://ethereum-magicians.org/t/eip-6150-hierarchical-nfts-an-extension-to-erc-721/12173 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-12-15 @@ -23,7 +23,7 @@ Hierarchy structure is commonly implemented for file systems by operating system ![Linux Hierarchical File Structure](../assets/eip-6150/linux-hierarchy.png) -Websites often use a directory and category hierarchy structure, such as eBay (Home -> Electronics -> Video Games -> Xbox -> Products), and Twitter (Home -> Lists -> List -> Tweets), and Reddit (Home -> r/ ethereum -> Posts -> Hot). +Websites often use a directory and category hierarchy structure, such as eBay (Home -> Electronics -> Video Games -> Xbox -> Products), and Twitter (Home -> Lists -> List -> Tweets), and Reddit (Home -> r/ethereum -> Posts -> Hot). ![Website Hierarchical Structure](../assets/eip-6150/website-hierarchy.png) @@ -52,11 +52,12 @@ In the future, with the development of the data availability solutions of Ethere 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. -Every [EIP-6150](./eip-6150.md) compliant contract must implement the [EIP-6150](./eip-6150.md), [EIP-721](./eip-721.md) and [EIP-165](./eip-165.md) interfaces. +Every compliant contract must implement this proposal, [EIP-721](./eip-721.md) and [EIP-165](./eip-165.md) interfaces. ```solidity pragma solidity ^0.8.0; +// Note: the ERC-165 identifier for this interface is 0x897e2c73. interface IERC6150 /* is IERC721, IERC165 */ { /** * @notice Emitted when `tokenId` token under `parentId` is minted. @@ -107,6 +108,7 @@ interface IERC6150 /* is IERC721, IERC165 */ { Optional Extension: Enumerable ```solidity +// Note: the ERC-165 identifier for this interface is 0xba541a2e. interface IERC6150Enumerable is IERC6150 /* IERC721Enumerable */ { /** * @notice Get total amount of children tokens under `parentId` token. @@ -143,6 +145,7 @@ interface IERC6150Enumerable is IERC6150 /* IERC721Enumerable */ { Optional Extension: Burnable ```solidity +// Note: the ERC-165 identifier for this interface is 0x4ac0aa46. interface IERC6150Burnable is IERC6150 { /** * @notice Burn the `tokenId` token. @@ -169,6 +172,7 @@ interface IERC6150Burnable is IERC6150 { Optional Extension: ParentTransferable ```solidity +// Note: the ERC-165 identifier for this interface is 0xfa574808. interface IERC6150ParentTransferable is IERC6150 { /** * @notice Emitted when the parent of `tokenId` token changed. @@ -204,6 +208,7 @@ interface IERC6150ParentTransferable is IERC6150 { Optional Extension: Access Control ```solidity +// Note: the ERC-165 identifier for this interface is 0x1d04f0b3. interface IERC6150AccessControl is IERC6150 { /** * @notice Check the account whether a admin of `tokenId` token. @@ -251,23 +256,23 @@ As mentioned in the abstract, this EIP's goal is to have a simple interface for All NFTs will make up a hierarchical relationship tree. Each NFT is a node of the tree, maybe as a root node or a leaf node, as a parent node or a child node. -[EIP-6150](./eip-6150.md) standardizes the event `Minted` to indicate the parent and child relationship when minting a new node. When a root node is minted, parentId should be zero. That means a token id of zero could not be a real node. So a real node token id must be greater than zero. +This proposal standardizes the event `Minted` to indicate the parent and child relationship when minting a new node. When a root node is minted, parentId should be zero. That means a token id of zero could not be a real node. So a real node token id must be greater than zero. -In a hierarchical tree, it's common to query upper and lower nodes. So [EIP-6150](./eip-6150.md) standardizes function `parentOf` to get the parent node of the specified node and standardizes function `childrenOf` to get all children nodes. +In a hierarchical tree, it's common to query upper and lower nodes. So this proposal standardizes function `parentOf` to get the parent node of the specified node and standardizes function `childrenOf` to get all children nodes. Functions `isRoot` and `isLeaf` can check if one node is a root node or a leaf node, which would be very useful for many cases. ### Enumerable Extension -[EIP-6150](./eip-6150.md) standardizes three functions as an extension to support enumerable queries involving children nodes. Each function all have param `parentId`, for compatibility, when the `parentId` specified zero means query root nodes. +This proposal standardizes three functions as an extension to support enumerable queries involving children nodes. Each function all have param `parentId`, for compatibility, when the `parentId` specified zero means query root nodes. ### ParentTransferable Extension -In some cases, such as filesystem, a directory or a file could be moved from one directory to another. So [EIP-6150](./eip-6150.md) adds ParentTransferable Extension to support this situation. +In some cases, such as filesystem, a directory or a file could be moved from one directory to another. So this proposal adds ParentTransferable Extension to support this situation. ### Access Control -In a hierarchical structure, usually, there is more than one account has permission to operate a node, like mint children nodes, transfer node, burn node. [EIP-6150](./eip-6150.md) adds a few functions as standard to check access control permissions. +In a hierarchical structure, usually, there is more than one account has permission to operate a node, like mint children nodes, transfer node, burn node. This proposal adds a few functions as standard to check access control permissions. ## Backwards Compatibility From 4c156ea983c18c0add9e93a63cf0be5891943707 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 4 Jan 2023 14:45:12 -0800 Subject: [PATCH 112/274] Add EIP-5982: Role-based Access Control (#5982) * Add ACL * Add ACL * Draft * Draft * Add events * Update EIPS/eip-5982.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5982.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5982.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update * Add role bytes32 def * Define role * Define role * Update eip-5982.md * Update eip-5982.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5982.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 EIPS/eip-5982.md diff --git a/EIPS/eip-5982.md b/EIPS/eip-5982.md new file mode 100644 index 00000000000000..ec9a80335aff52 --- /dev/null +++ b/EIPS/eip-5982.md @@ -0,0 +1,92 @@ +--- +eip: 5982 +title: Role-based Access Control +description: An interface for role-based access control for smart contracts. +author: Zainan Victor Zhou (@xinbenlv) +discussions-to: https://ethereum-magicians.org/t/eip-5982-role-based-access-control/11759 +status: Draft +type: Standards Track +category: ERC +created: 2022-11-15 +requires: 165, 5750 +--- + +## Abstract + +This EIP defines an interface for role-based access control for smart contracts. Roles are defined as `byte32`. The interface specifies how to read, grant, create and destroy roles. It specifies the sense of role power in the format of its ability to call a given method +identified by `bytes4` method selector. It also specifies how metadata of roles are represented. + +## Motivation + +There are many ways to establish access control for privileged actions. One common pattern is "role-based" access control, where one or more users are assigned to one or more "roles," which grant access to privileged actions. This pattern is more secure and flexible than ownership-based access control since it allows for many people to be granted permissions according to the principle of least privilege. + +## Specification + +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. + +Interfaces of reference is described as followed: + +```solidity +interface IERC_ACL_CORE { + function hasRole(bytes32 role, address account) external view returns (bool); + function grantRole(bytes32 role, address account) external; + function revokeRole(bytes32 role, address account) external; +} +``` + +```solidity +interface IERC_ACL_GENERAL { + event RoleGranted(address indexed grantor, bytes32 indexed role, address indexed grantee, bytes _data); + event RoleRevoked(address indexed revoker, bytes32 indexed role, address indexed revokee, bytes _data); + + event RoleCreated(address indexed roleGrantor, bytes32 role, bytes32 adminOfRole, string name, string desc, string uri, bytes32 calldata _data); + event RoleDestroyed(address indexed roleDestroyer, bytes32 role, bytes32 calldata _data); + event RolePowerSet(address indexed rolePowerSetter, bytes32 role, bytes4 methods, bytes calldata _data); + + function grantRole(bytes32 role, address account, bytes calldata _data) external; + function revokeRole(bytes32 role, address account, bytes calldata _data) external; + + function createRole(bytes32 role, bytes32 adminOfRole, string name, string desc, string uri, bytes32 calldata _data) external; + function destroyRole(bytes32 role, bytes32 calldata _data) external; + function setRolePower(bytes32 role, bytes4 methods, bytes calldata _data) view external returns(bool); + + function hasRole(bytes32 role, address account, bytes calldata _data) external view returns (bool); + function canGrantRole(bytes32 grantor, bytes32 grantee, bytes calldata _data) view external returns(bool); + function canRevokeRole(bytes32 revoker, bytes32 revokee, address account, bytes calldata _data) view external returns(bool); + function canExecute(bytes32 executor, bytes4 methods, bytes32 calldata payload, bytes calldata _data) view external returns(bool); +} +``` + +```solidity +interface IERC_ACL_METADATA { + function roleName(bytes32) external view returns(string); + function roleDescription(bytes32) external view returns(string); + function roleURI(bytes32) external view returns(string); +} +``` + +1. Compliant contracts MUST implement `IERC_ACL_CORE` +2. It is RECOMMENDED for compliant contracts to implement the optional extension `IERC_ACL_GENERAL`. +3. Compliant contracts MAY implement the optional extension `IERC_ACL_METADATA`. +4. A role in a compliant smart contract is represented in the format of `bytes32`. It's RECOMMENDED the value of such role is computed as a +`keccak256` hash of a string of the role name, in this format: `bytes32 role = keccak256("")`. such as `bytes32 role = keccak256("MINTER")`. +5. Compliant contracts SHOULD implement [EIP-165](./eip-165.md) identifier. + +## Rationale + +1. The names and parameters of methods in `IERC_ACL_CORE` are chosen to allow backward compatibility with OpenZeppelin's implementation. +2. The methods in `IERC_ACL_GENERAL` conform to [EIP-5750](./eip-5750.md) to allow extension. +3. The method of `renounceRole` was not adopted, consolidating with `revokeRole` to simplify interface. + + +## Backwards Compatibility + +Needs discussion. + +## Security Considerations + +Needs discussion. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 84b493573c37233f105ec39a723960631cd36e13 Mon Sep 17 00:00:00 2001 From: Bumblefudge Date: Thu, 5 Jan 2023 01:28:37 -0800 Subject: [PATCH 113/274] minor typos on EIP 5573 (#6257) --- EIPS/eip-5573.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-5573.md b/EIPS/eip-5573.md index cfbe42628a3783..781478bf6c3f49 100644 --- a/EIPS/eip-5573.md +++ b/EIPS/eip-5573.md @@ -165,9 +165,9 @@ The following is a non-normative example of a ReCap Capability Object with `def` } ``` -In the example above, the delegee is authorized to perform the action `read` independent of any resource, `append`, `delete` on resource `my.resource.1`, `append` on resource `my.resource.2` and `append` on `my.resource.3`. Note, the delegee can invoke each action invididually and independent from each other in the RS. Additionally the ReCap Capability Object contains some additional information that the RS will need during verification. The responsibility for defining the structure and semantics of this data lies with the RS. +In the example above, the delegee is authorized to perform the action `read` independent of any resource, `append`, `delete` on resource `my.resource.1`, `append` on resource `my.resource.2` and `append` on `my.resource.3`. Note, the delegee can invoke each action individually and independently from each other in the RS. Additionally the ReCap Capability Object contains some additional information that the RS will need during verification. The responsibility for defining the structure and semantics of this data lies with the RS. -It is expected that RS implementers define which resources they want to expose through ReCap Details Objects and which actions they want to allow to invoke on them. +It is expected that RS implementers define which resources they want to expose through ReCap Details Objects and which actions they want to allow users to invoke on them. #### ReCap Translation Algorithm @@ -270,4 +270,4 @@ TBD Resource service implementer's should not consider ReCaps as bearer tokens but instead require to authenticate the delegee in addition. The process of authenticating the delegee against the resource service is out of scope of this specification and can be done in various different ways. ## 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). From b2be680d5fb6a7515afc5d228e1eb858ce0d12cc Mon Sep 17 00:00:00 2001 From: Matt Stam Date: Thu, 5 Jan 2023 06:46:10 -0800 Subject: [PATCH 114/274] Update EIP-4337: fix miscellaneous typos (#6234) --- EIPS/eip-4337.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4337.md b/EIPS/eip-4337.md index 5ca80ed964d3d7..0d669ab8e78ede 100644 --- a/EIPS/eip-4337.md +++ b/EIPS/eip-4337.md @@ -36,9 +36,9 @@ This proposal takes a different approach, avoiding any adjustments to the consen ### Definitions -* **UserOperation** - a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is named "transaction". +* **UserOperation** - a structure that describes a transaction to be sent on behalf of a user. To avoid confusion, it is not named "transaction". * Like a transaction, it contains "sender", "to", "calldata", "maxFeePerGas", "maxPriorityFee", "signature", "nonce" - * unlike transaction, it contains several other fields, described below + * unlike a transaction, it contains several other fields, described 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. @@ -203,7 +203,7 @@ In the execution loop, the `handleOps` call must perform the following steps for ![](../assets/eip-4337/image1.png) 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 +A node/bundler SHOULD drop (not add to the mempool) a `UserOperation` that fails the validation ### Extension: paymasters From c9d646c9dd0208372baf23c0958134c56f047330 Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Thu, 5 Jan 2023 08:56:11 -0700 Subject: [PATCH 115/274] 3540: restrict number of code sections to 1024 (#6251) --- EIPS/eip-3540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index a9b51afac6c92e..b9a3fcfc31f490 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -125,7 +125,7 @@ type_section := (inputs, outputs, max_stack_height)+ | kind_type | 1 byte | 0x01 | kind marker for EIP-4750 type section header | | type_size | 2 bytes | 0x0004-0xFFFC | uint16 denoting the length of the type section content, 4 bytes per code segment | | kind_code | 1 byte | 0x02 | kind marker for code size section | -| num_code_sections | 2 bytes | 0x0001-0xFFFF | uint16 denoting the number of the code sections | +| num_code_sections | 2 bytes | 0x0001-0x0400 | uint16 denoting the number of the code sections | | code_size | 2 bytes | 0x0001-0xFFFF | uint16 denoting the length of the code section content | | kind_data | 1 byte | 0x03 | kind marker for data size section | | data_size | 2 bytes | 0x0000-0xFFFF | uint16 integer denoting the length of the data section content | From 2101931bac496e5b25f76262d3bfd210b112294a Mon Sep 17 00:00:00 2001 From: Firn Protocol <93839494+firnprotocol@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:25:24 -0500 Subject: [PATCH 116/274] another update to EIP-5630: only do ECDH (#6262) * another update to EIP-5630: only do ECDH * add missing blank lines * decrypt --> performECDH --- EIPS/eip-5630.md | 95 ++++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 36 deletions(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index ad56f63de637b6..3015bc13359a46 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -13,7 +13,7 @@ created: 2022-09-07 ## Abstract -This EIP proposes a new way to encrypt and decrypt using Ethereum keys. This EIP uses _only_ the `secp256k1` curve, and it uses a standardized version of ECIES. In contrast, a previous EIPs used the same secret key, in both signing and encryption, on two _different_ curves (namely, `secp256k1` and `ec25519`). +This EIP proposes a new way to encrypt and decrypt using Ethereum keys. This EIP uses _only_ the `secp256k1` curve, and proposes two new RPC methods: `eth_getEncryptionPublicKey` and `eth_performECDH`. These two methods, in conjunction, allow users to receive encryptions and perform decryptions (respectively). We require that the wallet _only_ perform the core ECDH operation, leaving the ECIES operations up to implementers (we do suggest a standardized version of ECIES, however). In contrast, a previous EIPs used the same secret key, in both signing and encryption, on two _different_ curves (namely, `secp256k1` and `ec25519`), and hardcoded a particular version of ECIES. ## Motivation @@ -23,24 +23,34 @@ We discuss a second sort of example. In a certain common design pattern, a dApp ## Specification -We describe our approach here; we compare our approach to prior EIPs in the **Rationale** section below. +We describe our approach here; we compare our approach to prior EIPs in the **Rationale** section below. Throughout, we make reference to SEC 1: Elliptic Curve Cryptography, by Daniel R. L. Brown. We use the `secp256k1` curve for both signing and encryption. -For encryption, we use ECIES. Specifically, we propose the standardized choices: +For encryption, we use ECIES. We specify that the wallet _only_ perform the sensitive ECDH operation. This lets implementers select their own ECIES variants at will. + +We propose that all binary data be serialized to and from `0x`-prefixed hex strings. We moreover use `0x`-prefixed hex strings to specify private keys and public keys, and represent public keys in compressed form. We represent Ethereum accounts in the usual way (`0x`-prefixed, 20-byte hex strings). Specifically, to serialize and deserialize elliptic curve points, implementers MUST use the following standard: + +- to serialize a point: use [SEC 1, §2.3.3], with point compression. +- to deserialize a point: use [SEC 1, §2.3.3], while _requiring_ point compression; that is: + + - the input byte string MUST have length ⌈log₂q / 8⌉ + 1 = `33`. + - the first byte MUST be `0x02` or `0x03`. + - that the integer represented by the remaining 32 bytes (as in [SEC 1, §2.3.8]) MUST reside in {1, ..., _p_ - 1}, and moreover MUST yield a quadratic residue modulo _p_ under the Weierstrass expression X^3 + 7 (modulo _p_). + +For application-level implementers actually implementing ECIES, we propose the following variant. Unless they have a reason to do otherwise, implementers SHOULD use the following standardized choices: - the KDF `ANSI-X9.63-KDF`, where the hash function `SHA-512` is used, - the HMAC `HMAC–SHA-256–256 with 32 octet or 256 bit keys`, - the symmetric encryption scheme `AES–256 in CBC mode`. -We propose that the binary, _concatenated_ serialization mode for ECIES ciphertexts be used, both for encryption and decryption, where moreover elliptic curve points are _compressed_. This approach is considerably more space-efficient than the prior approach, which outputted a stringified JSON object (itself containing base64-encoded fields). -We moreover propose that binary data be serialized to and from `0x`-prefixed hex strings. We moreover use `0x`-prefixed hex strings to specify private keys and public keys, and represent public keys in compressed form. We represent Ethereum accounts in the usual way (`0x`-prefixed, 20-byte hex strings). +We propose that the binary, _concatenated_ serialization mode for ECIES ciphertexts be used, both for encryption and decryption, where moreover elliptic curve points are _compressed_. Thus, on the request: ```javascript request({ method: 'eth_getEncryptionPublicKey', - params: [account], + params: [account] }) ``` @@ -48,26 +58,23 @@ where `account` is a standard 20-byte, `0x`-prefixed, hex-encoded Ethereum accou - find the secret signing key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. - compute the `secp256k1` public key corresponding to `sk`. -- return this public key in compressed, `0x`-prefixed, hex-encoded form. +- return this public key in compressed, `0x`-prefixed, hex-encoded form, following [SEC 1, §2.3.3]. On the request ```javascript request({ - method: 'eth_decrypt', - params: [encryptedMessage, account], + method: 'eth_performECDH', + params: [account, ephemeralKey] }) ``` -where `account` is as above, and `encryptedMessage` is a JSON object with the properties `version` (an arbitrary string) and `ciphertext` (a `0x`-prefixed, hex-encoded, bytes-like string), the client should operate as follows: +where `account` is as above, and `ephemeralKey` is an elliptic curve point encoded as above: -- perform a `switch` on the value `encryptedMessage.version`. if it equals: - - `secp256k1-sha512kdf-aes256cbc-hmacsha256`, then break from the switch and proceed as in the bullets below; - - `x25519-xsalsa20-poly1305`, then, optionally, use #1098's specification _if_ backwards compatibility is desired, and otherwise fallthrough; - - `default`, throw an error. - find the secret key `sk` corresponding to the Ethereum account `account`, or else return an error if none exists. -- using `sk`, perform an ECIES decryption of `encryptedMessage.ciphertext`, where the above choices of parameters are used. -- decode the resulting binary plaintext as a `utf-8` string, and return it. +- deserialize `ephemeralKey` to an elliptic curve point using [SEC 1, §2.3.3] (where compression is required), throwing an error if deserialization fails. +- compute the elliptic curve Diffie–Hellman secret, following [SEC 1, §3.3.1]. +- return the resulting field element as an 0x-prefixed, hex-encoded, 32-byte string, using [SEC 1, §2.3.5]. Test vectors are given below. @@ -94,7 +101,7 @@ contract ERC5630 { } ``` -Each contract should implement `encryptToAccount` as it desires; for example, it could use our specification above (i.e., for some fixed public key depending on the contract), or something arbitrary. +Each contract MAY implement `encryptTo` as it desires. Unless it has a good reason to do otherwise, it SHOULD use the ECIES variant we propose above. ## Rationale @@ -107,19 +114,26 @@ We now discuss a few further aspects of our approach. **On-chain public key discovery.** Our proposal has an important feature whereby an encryption _to_ some account can be constructed whenever that account has signed at least one transaction. Indeed, it is possible to recover an account's `secp256k1` public key directly from any signature on behalf of that account. +**ECDH vs. ECIES.** We specify that the wallet _only_ perform the sensitive ECDH operation, and let application-level implementers perform the remaining steps of ECIES. This has two distinct advantages: + +- **Flexibility.** It allows implementers to select arbitrary variants of ECIES, without having to update what the wallet does. +- **Bandwidth.** Our approach requires that only small messages (on the order of 32 bytes) be exchanged between the client and the wallet. This could be material in settings in which the plaintexts and ciphertexts at play are large, and when the client and the wallet are separated by an internet connection. + **Twist attacks.** A certain GitHub post by Christian Lundkvist warns against "twist attacks" on the `secp256k1` curve. These attacks are not applicable to this EIP, for multiple _distinct_ reasons, which we itemize: -- **Only applies to ECDH, not ECIES.** This attack only applies to a scenario in which an attacker can induce a victim to exponentiate an attacker-supplied point by a sensitive scalar, and then moreover send the result back to the attacker. But this pattern only happens in ECDH, and never ECIES. Indeed, in ECIES, we recall that the only sensitive Diffie–Hellman operation happens during decryption, but in this case, the victim (who would be the decryptor) never sends the resulting DH point back to the attacker (rather, the victim merely uses it locally to attempt an AES decryption). During _encryption_, the exponentiation is done by the encryptor, who has no secret at all (sure enough, the exponentiation is by an ephemeral scalar), so here there would be nothing for the attacker to learn. -- **Only applies to uncompressed points.** Indeed, we use compressed points in this EIP; when compressed points are used, any 32-byte string supplied by an attacker will resolve canonically to a point on the right curve, or else generate an error; there is no possibility of a "wrong curve" point. -- **Only applies when you fail to check a point is on the curve.** But this is inapplicable for us anyway, since we use compressed points (see above). +- **Only applies to classical ECDH, not ECIES.** This attack only applies to classical ECDH (i.e., in which both parties use persistent, authenticated public keys), and not to ECIES (in which one party, the encryptor, uses an ephemeral key). Indeed, it only applies to a scenario in which an attacker can induce a victim to exponentiate an attacker-supplied point by a sensitive scalar, and then moreover send the result back to the attacker. But this pattern only happens in classical Diffie–Hellman, and never in ECIES. Indeed, in ECIES, we recall that the only sensitive Diffie–Hellman operation happens during decryption, but in this case, the victim (who would be the decryptor) never sends the resulting DH point back to the attacker (rather, the victim merely uses it locally to attempt an AES decryption). During _encryption_, the exponentiation is done by the encryptor, who has no secret at all (sure enough, the exponentiation is by an ephemeral scalar), so here there would be nothing for the attacker to learn. +- **Only applies to uncompressed points.** Indeed, we use compressed points in this EIP. When compressed points are used, each 33-byte string _necessarily_ either resolves to a point on the correct curve, or else has no reasonable interpretation. There is no such thing as "a point not on the curve" (which, in particular, can pass undetectedly as such). +- **Only applies when you fail to check a point is on the curve.** But this is inapplicable for us anyway, since we use compressed points (see above). We also require that all validations be performed. ## Backwards Compatibility -The previous EIP stipulated that encryption and decryption requests contain a `version` string. Our proposal merely adds a case for this string; encryption and decryption requests under the existing scheme will be handled identically. -The previous proposal did _not_ include a version string in `encryptionPublicKey`, and merely returned the `ec25519` public key directly as a string. We thus propose to immediately return the `secp256k1` public key, overwriting the previous behavior. +Our `eth_performECDH` method is new, and so doesn't raise any backwards compatibility issues. + +A previous proposal proposed an `eth_getEncryptionPublicKey` method (together with an `eth_decrypt` method unrelated to this EIP). Our proposal overwrites the previous behavior of `eth_getEncryptionPublicKey`. It is unlikely that this will be an issue, since encryption keys need be newly retrieved _only_ upon the time of encryption; on the other hand, _new_ ciphertexts will be generated using our new approach. +(In particular, our modification will not affect the ability of ciphertexts generated using the old EIP to be `eth_decrypt`ed.) -In any case, the previous EIP was never standardized, and is _not_ (to our knowledge) implemented in a non-deprecated manner in _any_ production code today. We thus have a lot of flexibility here; we only need enough backwards compatibility to allow dApps to migrate. +In any case, the previous EIP was never standardized, and is _not_ (to our knowledge) implemented in a non-deprecated manner in _any_ production code today. ### Test Cases @@ -140,7 +154,7 @@ Thus, the request: ```javascript request({ method: 'eth_getEncryptionPublicKey', - params: ["0x72682F2A3c160947696ac3c9CC48d290aa89549c"], + params: ["0x72682F2A3c160947696ac3c9CC48d290aa89549c"] }) ``` @@ -150,28 +164,37 @@ should return: "0x03ff5763a2d3113229f2eda8305fae5cc1729e89037532a42df357437532770010" ``` -Encrypting the UTF-8 message `I use Firn Protocol to gain privacy on Ethereum.` under the above public key could yield, for example: +If an encryptor were to encrypt a message—say, `I use Firn Protocol to gain privacy on Ethereum.`—under the above public key, using the above ECIES variant, he could obtain, for example: ```javascript -{ - version: 'secp256k1-sha512kdf-aes256cbc-hmacsha256', - ciphertext: '0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce', -} +"0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce" ``` -Therefore, the request +Upon obtaining this ciphertext, the decryptor would extract the relevant ephemeral public key, namely: + +```javascript +"0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf8" +``` + +And submit the request: ```javascript request({ - method: 'eth_decrypt', - params: [{ - version: 'secp256k1-sha512kdf-aes256cbc-hmacsha256', - ciphertext: '0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf850e6c2af8fb38e3e31d679deac82bd12148332fa0e34aecb31981bd4fe8f7ac1b74866ce65cbe848ee7a9d39093e0de0bd8523a615af8d6a83bbd8541bf174f47b1ea2bd57396b4a950a0a2eb77af09e36bd5832b8841848a8b302bd816c41ce', - }, "0x72682F2A3c160947696ac3c9CC48d290aa89549c"], + method: 'eth_performECDH', + params: [ + "0x72682F2A3c160947696ac3c9CC48d290aa89549c", + "0x036f06f9355b0e3f7d2971da61834513d5870413d28a16d7d68ce05dc78744daf8" + ] }) ``` -should return the string `I use Firn Protocol to gain privacy on Ethereum.`. +which in turn would return the Diffie–Hellman secret: + +```javascript +"0x4ad782e7409702101abe6d0279f242a2c545c46dd50a6704a4b9e3ae2730522e" +``` + +Upon proceeding with the above ECIES variant, the decryptor would then obtain the string `I use Firn Protocol to gain privacy on Ethereum.`. ## Security Considerations From 15c0e2b8a488d21852680d0dea39d6b3ba22910d Mon Sep 17 00:00:00 2001 From: Firn Protocol <93839494+firnprotocol@users.noreply.github.com> Date: Thu, 5 Jan 2023 13:51:18 -0500 Subject: [PATCH 117/274] very minor edits (#6263) --- EIPS/eip-5630.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index 3015bc13359a46..a604a1c886e1f3 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -35,7 +35,7 @@ We propose that all binary data be serialized to and from `0x`-prefixed hex stri - the input byte string MUST have length ⌈log₂q / 8⌉ + 1 = `33`. - the first byte MUST be `0x02` or `0x03`. - - that the integer represented by the remaining 32 bytes (as in [SEC 1, §2.3.8]) MUST reside in {1, ..., _p_ - 1}, and moreover MUST yield a quadratic residue modulo _p_ under the Weierstrass expression X^3 + 7 (modulo _p_). + - the integer represented by the remaining 32 bytes (as in [SEC 1, §2.3.8]) MUST reside in {0, ..., _p_ - 1}, and moreover MUST yield a quadratic residue modulo _p_ under the Weierstrass expression X^3 + 7 (modulo _p_). For application-level implementers actually implementing ECIES, we propose the following variant. Unless they have a reason to do otherwise, implementers SHOULD use the following standardized choices: From c3a1a5db84e81d713a5e28d97443d23732324998 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Thu, 5 Jan 2023 20:51:26 -0500 Subject: [PATCH 118/274] Add EIP-6188: Nonce Cap (#6188) * Cap Nonce * Assign EIP-6188 * Rename eip-nonce-cap.md to eip-6188.md * Add discussions URL * Remove circular link * Apply suggestions from code review * Oops yea that shouldn't be allowed, either Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * GitHub Copilot fail Other people: "Wait, what do you mean 90% of this was written by an AI?" * Add note saying why there are no security considerations Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-6188.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 EIPS/eip-6188.md diff --git a/EIPS/eip-6188.md b/EIPS/eip-6188.md new file mode 100644 index 00000000000000..3b6cb1da12716b --- /dev/null +++ b/EIPS/eip-6188.md @@ -0,0 +1,48 @@ +--- +eip: 6188 +title: Nonce Cap +description: Caps the nonce at 2^64-2 +author: Pandapip1 (@Pandapip1) +discussions-to: https://ethereum-magicians.org/t/eip-6190-functional-selfdestruct/12232 +status: Draft +type: Standards Track +category: Core +created: 2022-12-20 +requires: 2929 +--- + +## Abstract + +This EIP caps the nonce at `2^64-2`, reserving it for contracts with unusual behavior, as defined in other EIPs. + +## Motivation + +This EIP is not terribly useful on its own, as it adds additional computation without any useful side effects. However, it can be used by other EIPs. + +## 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 and RFC 8174. + +### EOA Transactions + +The nonce of a transaction originating from an EOA MUST be less than `2^64-2`. If the nonce is either `2^64-1` or `2^64-2`, the transaction MUST be invalid. + +### `CREATE` and `CREATE2` + +If a nonce would be incremented to `2^64-1` by `CREATE` or `CREATE2`, it is instead set to `2^64-2`. `2^64-1` is reserved for alias or other special contracts. + +## Rationale + +Capping a nonce allows for contracts with special properties to be created, with their functionality based on their contract code. As such, only one nonce needs to be reserved. + +## Backwards Compatibility + +This EIP requires a protocol upgrade, since it modifies consensus rules. The further restriction of nonce should not have an effect on accounts, as reaching a nonce of `2^64-2` is unfeasible. + +## Security Considerations + +As it is infeasible for contract accounts to get to the nonce limit, any potential problems with opcodes that depend on the value of an account's nonce can be safely ignored. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From a82f8407166bc2c2244000994d8d7472f46c0276 Mon Sep 17 00:00:00 2001 From: Zergity <37166829+Zergity@users.noreply.github.com> Date: Fri, 6 Jan 2023 10:53:29 +0700 Subject: [PATCH 119/274] Update EIP-6120: more contents and token-transfer sub-action (#6259) * remove unnessary output token.amount > 0 condition * security consideration for AMOUNT_INS_PLACEHOLDER * no need to specify memory or calldata for bytes type * support token-transfer sub-action and add more contents * address CI issue * texts and comments update for clarification --- EIPS/eip-6120.md | 272 ++++++++++++++++++++++++++++++----------------- 1 file changed, 175 insertions(+), 97 deletions(-) diff --git a/EIPS/eip-6120.md b/EIPS/eip-6120.md index 71757b910dfff4..13fbd8a7a34e96 100644 --- a/EIPS/eip-6120.md +++ b/EIPS/eip-6120.md @@ -38,10 +38,12 @@ The Universal Token Router function arguments can act as a manifest for users wh Application contracts follow this standard can use the Universal Token Router to have the following benefits: -* Share the user token allowance with all other applications. +* Safely share the user token allowance with all other applications. * Freely update their helper contract logic. * Save development and security audit costs on router contracts. +The Universal Token Router promotes the **security-by-result** model in decentralized applications instead of **security-by-process**. By directly querying token balance change for output verification, user transactions can be secured even while interacting with erroneous or malicious contracts. With non-token results, application helper contracts can provide additional result-checking functions for UTR's output action. + ## Specification ```solidity @@ -50,7 +52,8 @@ struct Token { address adr; // token contract address uint id; // token id for EIP721 and EIP1155 uint amount; // amountInMax for input action, amountOutMin for output action - uint offset; // byte offset to get the amountIn from the last inputParams + uint offset; // with input actions: byte offset to get the amountIn from the lastInputResult bytes + // with output actions: 0 for token balance change verification, or output token transfer address recipient; } ``` @@ -74,36 +77,50 @@ interface IUniversalTokenRouter { Actions with `action.output == 0` declare which and how many tokens are transferred from `msg.sender` to `token.recipient`. -1. If the `action.data` is not empty, `action.code.call(action.data)` is executed and the value returned is recorded in `amountIns` as a `bytes` for subsequence token transfer amounts. +1. If the `action.data` is not empty, `action.code.call(action.data)` is executed and the value returned is recorded in `lastInputResult` as a `bytes` for subsequence token transfer amounts. 2. For each `token` in `action.tokens`: * (a) The amount of token to be transferred is determined as: * if `offset < 32`, `amountIn = token.amount` - * if `offset >= 32`, `amountIn = amountIns.slice(offset-32, offset)` - * `amountIn` MUST NOT greater than `token.amount`, otherwise, transaction will be reverted with `EXCESSIVE_INPUT_AMOUNT` reason. - * (b) If the token is `ETH` and the `recipient` is `0x0`, step **(c)** is skipped and the `amountIn` will be passed to the next output action as the transaction value. + * if `offset >= 32`, `amountIn = lastInputResult.slice(offset-32, offset)` + * `amountIn` MUST NOT be greater than `token.amount`, otherwise, the transaction will be reverted with `EXCESSIVE_INPUT_AMOUNT`. + * (b) If the token is `ETH` and the `recipient` is `0x0`, the next step **(c)** is skipped and the `amountIn` will be passed to the next output action as the transaction value. * (c) Transfer `amountIn` of token from `msg.sender` to `recipient`. -Note: `amountIns` is the last value returned by an input action's code contract call. It can be shared by multiple input actions until another action's code is executed. Using `amountIns` (by passing `offset >= 32`) before any input action execution can produce unexpected `amountIn` value. +Note: `lastInputResult` is the last value returned by an input action's code contract call. It can be shared by multiple input actions until another action's code is executed. Using `lastInputResult` before any input action execution can produce unexpected `amountIn` values. ### Output Action -Actions with `action.output > 0` declare the main application action to execute, and optionally verify the output tokens after all is done. +Actions with `action.output > 0` declare the main application action to execute, and optionally transfer and verify the output tokens after that. -1. For each `token` in `action.tokens` with `amount > 0`, the current token balance of `recipient` is recorded for later verification. -2. Execute the `action.code.call{value: value}(action.data)`, where `value` can be zero or the `amountIn` of the last `ETH` input with `recipient == 0x0` (see Input Action 2b). -3. `action.code` execution failure (revert) can be ignored by passing `action.output == 2`. +1. If the `action.data` is not empty, execute the `action.code.call{value: value}(action.data)`, where: + * `value` can be zero or the `amountIn` of the last `ETH` input with `recipient == 0x0` (see Input Action 2b), + * any failure of the output execution will be ignored if the `action.output == 2` +2. For each `token` in `action.tokens`: + * if `token.offset > 0`, transfer the following `amountOut` of token from `this` (UTR contract) to `token.recipient`: + * if `token.offset >= 32`, `amountOut = lastInputResult.slice(offset-32, offset)` + * if `token.offset == 1`: + * if `token.amount > 0`, `amountOut = token.amount`, + * if `token.amount == 0`, `amountOut` is the current token balance owned by `this` (UTR contract). + * if `token.offset == 0`, verify the balance change of `token.recipient`. The balance change MUST NOT be less than `token.amount` of each token, otherwise, the transaction will be reverted with `INSUFFICIENT_OUTPUT_AMOUNT`. -The last `amountIns` bytes can be passed to an output action code execution by: +The last `lastInputResult` bytes can be passed to an output action code execution by: -* using a function with the last param is a `bytes memory` or `bytes calldata` type. +* using a function with the last param as a `bytes` type. * pass the following value to that last param in the output `action.data`: - `AMOUNT_INS_PLACEHOLDER = keccak256('UniversalTokenRouter.AMOUNT_INS_PLACEHOLDER')` + +``` +LAST_INPUT_RESULT = keccak256('UniversalTokenRouter.LAST_INPUT_RESULT') +``` ### Output Token Verification -After all the actions are handled as above, every token balance tracked in Output Action #1 is queried again for comparison. The balance change MUST NOT be less than `amount` of each token, otherwise, transaction will be reverted with `INSUFFICIENT_OUTPUT_AMOUNT` reason. +At the very beginning of the `exec` function, for all output actions, each token with `token.offset == 0` has its balance of `token.recipient` recorded for later verification. After each output action, those token balances are queried again for comparison. -A special id `EIP_721_ALL = keccak256('UniversalTokenRouter.EIP_721_ALL')` is reserved for EIP-721, which can be used in output actions to verify the total amount of all ids owned by the `recipient` address. +A special id `EIP_721_ALL` is reserved for EIP-721, which can be used in output actions to verify the total amount of all ids owned by the `recipient` address. + +``` +EIP_721_ALL = keccak256('UniversalTokenRouter.EIP_721_ALL') +``` ### Usage Samples @@ -144,8 +161,8 @@ UniversalTokenRouter.exec([{ eip: 20, token: path[0], id: 0, + offset: 0, // use exact amount specified bellow amount: amountIn, - offset: 0, // use amount specified above recipient: UniswapV2Library.pairFor(factory, path[0], path[1]), }], }, { @@ -153,11 +170,11 @@ UniversalTokenRouter.exec([{ code: UniswapV2Helper01.address, data: encodeFunctionData("swapExactTokensForTokens", [amountIn, amountOutMin, path, to, deadline]), tokens: [{ + offset: 0, // balance change verification eip: 20, token: path[path.length-1], id: 0, amount: amountOutMin, - offset: 0, // unused for output action recipient: to, }], }]) @@ -197,26 +214,26 @@ UniversalTokenRouter.exec([{ eip: 20, token: path[0], id: 0, + offset: 64, // first item of getAmountIns result array amount: amountInMax, - offset: 64, // first item of getAmountIns result recipient: UniswapV2Library.pairFor(factory, path[0], path[1]), }], }, { output: 1, code: UniswapV2Helper01.address, - data: encodeFunctionData("swap", [path, to, AMOUNT_INS_PLACEHOLDER]), + data: encodeFunctionData("swap", [path, to, LAST_INPUT_RESULT]), tokens: [{ + offset: 0, // balance change verification eip: 20, token: path[path.length-1], id: 0, amount: amountOut, - offset: 0, // unused for output action recipient: to, }], }]) ``` -The result of input action's `getAmountIns` will replace the `AMOUNT_INS_PLACEHOLDER` bytes, save the transaction from calculating twice with the same data. +The result of input action's `getAmountIns` will replace the `LAST_INPUT_RESULT` bytes, save the transaction from calculating twice with the same data. #### `UniswapRouter.addLiquidity` @@ -253,15 +270,15 @@ UniversalTokenRouter.exec([{ eip: 20, token: tokenA, id: 0, - amount: amountADesired, - offset: 32, // first item of _addLiquidity result + offset: 32, // first item of _addLiquidity results + amount: amountADesired, // amountInMax recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB), }, { eip: 20, token: tokenB, id: 0, - amount: amountBDesired, - offset: 64, // second item of _addLiquidity result + offset: 64, // second item of _addLiquidity results + amount: amountBDesired, // amountInMax recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB), }], }, { @@ -269,11 +286,11 @@ UniversalTokenRouter.exec([{ code: UniswapV2Library.pairFor(factory, tokenA, tokenB), data: encodeFunctionData("mint", [to]), tokens: [{ + offset: 0, // balance change verification eip: 20, token: UniswapV2Library.pairFor(factory, tokenA, tokenB), id: 0, - amount: 1, // just enough to verify the correct recipient - offset: 0, // unused for output action + amount: 1, // amountOutMin: just enough to verify the correct recipient recipient: to, }], }]) @@ -289,43 +306,128 @@ Flashloan transactions are out of scope since it requires support from the appli ## Backwards Compatibility +### Tokens + Old token contracts (EIP-20, EIP-721 and EIP-1155) require approval for the Universal Token Router once for each account. New token contracts can pre-configure the Universal Token Router as a trusted spender, and no approval transaction is required. +### Application Contracts + +All application contracts that accept `recipient` (or `to`) argument instead of using `msg.sender` as the beneficiary address are compatible with the UTR out of the box. + +Application contracts that transfer tokens (EIP-20, EIP-721, and EIP-1155) to `msg.sender` can use the UTR output token transfer sub-action to re-direct tokens to another `recipient` address. + +```javascript +// sample code to deposit WETH and transfer them out +UniversalTokenRouter.exec([{ + output: 0, + code: AddressZero, + data: '0x', + tokens: [{ + eip: 0, // ETH + adr: AddressZero, + id: 0, + offset: 0, // use the exact amount specified bellow + amount: 123, + recipient: AddressZero, // pass it as the value for the next output action + }], +}, { + output: 1, + code: WETH.address, + data: encodeFunctionData('deposit', []), // WETH.deposit returns WETH token to the UTR contract + tokens: [{ + offset: 1, // token transfer sub-action + eip: 20, + adr: WETH.address, + id: 0, + amount: 0, // entire WETH balance of this UTR contract + recipient: SomeRecipient, + }], +}, { + // ... continue to use WETH in SomeRecipient +}], {value: 123}) +``` + +Applications can also deploy additional adapter contracts to add a `recipient` to their functions. + +```solidity +// sample adapter contract for WETH +contract WethAdapter { + address immutable WETH = 0x....; + function deposit(address recipient) external payable { + IWETH(WETH).deposit(){value: msg.value}; + TransferHelper.safeTransfer(WETH, recipient, msg.value); + } +} +``` + +Application contracts that use `msg.sender` as the beneficiary address in their internal storage without any function for ownership transfer are incompatible with the UTR. + ## Reference Implementation ```solidity contract UniversalTokenRouter is IUniversalTokenRouter { - uint constant AMOUNT_INS_PLACEHOLDER = uint(keccak256('UniversalTokenRouter.AMOUNT_INS_PLACEHOLDER')); + uint constant LAST_INPUT_RESULT = uint(keccak256('UniversalTokenRouter.LAST_INPUT_RESULT')); uint constant EIP_721_ALL = uint(keccak256('UniversalTokenRouter.EIP_721_ALL')); function exec( Action[] calldata actions ) override external payable { unchecked { - uint[][] memory amounts = new uint[][](actions.length); + // track the balances before any action is executed + uint[][] memory balances = new uint[][](actions.length); + for (uint i = 0; i < actions.length; ++i) { + if (actions[i].output == 0 || actions[i].tokens.length == 0) { + continue; + } + balances[i] = new uint[](actions[i].tokens.length); + for (uint j = 0; j < balances[i].length; ++j) { + if (actions[i].tokens[j].offset == 0) { + balances[i][j] = _balanceOf(actions[i].tokens[j], actions[i].tokens[j].recipient); + } + } + } + uint value; // track the ETH value to pass to next output action transaction value - bytes memory amountIns; + bytes memory lastInputResult; for (uint i = 0; i < actions.length; ++i) { Action memory action = actions[i]; - if (action.output > 0) { - // output action - amounts[i] = new uint[](action.tokens.length); + if (action.output == 0) { + // input action + if (action.data.length > 0) { + bool success; + (success, lastInputResult) = action.code.call(action.data); + if (!success) { + assembly { + revert(add(lastInputResult,32),mload(lastInputResult)) + } + } + } for (uint j = 0; j < action.tokens.length; ++j) { Token memory token = action.tokens[j]; - if (token.amount > 0) { - // track the recipient balance before the action is executed - amounts[i][j] = _balanceOf(token); + if (token.offset >= 32) { + // require(inputParams.length > 0, "UniversalTokenRouter: OFFSET_OF_EMPTY_INPUT"); + uint amount = _sliceUint(lastInputResult, token.offset); + require(amount <= token.amount, "UniversalTokenRouter: EXCESSIVE_INPUT_AMOUNT"); + token.amount = amount; + } + if (token.eip == 0 && token.recipient == address(0x0)) { + value = token.amount; + // ETH not transfered here will be passed to the next output call value + } else if (token.amount > 0) { + _transferFrom(token, msg.sender); } } + } else { + // output action if (action.data.length > 0) { uint length = action.data.length; if (length >= 4+32*3 && - _sliceUint(action.data, length) == AMOUNT_INS_PLACEHOLDER && + _sliceUint(action.data, length) == LAST_INPUT_RESULT && _sliceUint(action.data, length-32) == 32) { - action.data = _concat(action.data, length-32, amountIns); + action.data = _concat(action.data, length-32, lastInputResult); } (bool success, bytes memory result) = action.code.call{value: value}(action.data); // ignore output action error if output == 2 @@ -336,65 +438,45 @@ contract UniversalTokenRouter is IUniversalTokenRouter { } delete value; // clear the ETH value after transfer } - continue; - } - // input action - if (action.data.length > 0) { - bool success; - (success, amountIns) = action.code.call(action.data); - if (!success) { - assembly { - revert(add(amountIns,32),mload(amountIns)) + for (uint j = 0; j < action.tokens.length; ++j) { + Token memory token = actions[i].tokens[j]; + if (token.offset > 0) { + // token transfer sub-action + if (token.offset >= 32) { + token.amount = _sliceUint(lastInputResult, token.offset); + } else if (token.amount == 0) { + token.amount = _balanceOf(token, address(this)); + } + _transferFrom(token, address(this)); + } else { + // verify the balance change + uint balance = _balanceOf(token, token.recipient); + uint change = balance - balances[i][j]; // overflow checked with `change <= balance` bellow + require(change >= token.amount && change <= balance, 'UniversalTokenRouter: INSUFFICIENT_OUTPUT_AMOUNT'); } } } - for (uint j = 0; j < action.tokens.length; ++j) { - Token memory token = action.tokens[j]; - if (token.offset >= 32) { - uint amount = _sliceUint(amountIns, token.offset); - require(amount <= token.amount, "UniversalTokenRouter: EXCESSIVE_INPUT_AMOUNT"); - token.amount = amount; - } - if (token.eip == 0 && token.recipient == address(0x0)) { - value = token.amount; - continue; // ETH not transfered here will be passed to the next output call value - } - if (token.amount > 0) { - _transfer(token); - } - } } + // refund any left-over ETH uint leftOver = address(this).balance; if (leftOver > 0) { TransferHelper.safeTransferETH(msg.sender, leftOver); } - // verify the balance change - for (uint i = 0; i < actions.length; ++i) { - if (actions[i].output == 0) { - continue; - } - for (uint j = 0; j < actions[i].tokens.length; ++j) { - Token memory token = actions[i].tokens[j]; - if (token.amount == 0) { - continue; - } - uint balance = _balanceOf(token); - uint change = balance - amounts[i][j]; // overflow checked with `change <= balance` bellow - require(change >= token.amount && change <= balance, 'UniversalTokenRouter: INSUFFICIENT_OUTPUT_AMOUNT'); - amounts[i][j] = change; - } - } } } - function _transfer(Token memory token) internal { + function _transferFrom(Token memory token, address from) internal { unchecked { if (token.eip == 20) { - TransferHelper.safeTransferFrom(token.adr, msg.sender, token.recipient, token.amount); + if (from == address(this)) { + TransferHelper.safeTransfer(token.adr, token.recipient, token.amount); + } else { + TransferHelper.safeTransferFrom(token.adr, from, token.recipient, token.amount); + } } else if (token.eip == 1155) { - IERC1155(token.adr).safeTransferFrom(msg.sender, token.recipient, token.id, token.amount, ""); + IERC1155(token.adr).safeTransferFrom(from, token.recipient, token.id, token.amount, ""); } else if (token.eip == 721) { - IERC721(token.adr).safeTransferFrom(msg.sender, token.recipient, token.id); + IERC721(token.adr).safeTransferFrom(from, token.recipient, token.id); } else if (token.eip == 0) { TransferHelper.safeTransferETH(token.recipient, token.amount); } else { @@ -402,27 +484,26 @@ contract UniversalTokenRouter is IUniversalTokenRouter { } } } - function _balanceOf(Token memory token) internal view returns (uint balance) { + function _balanceOf(Token memory token, address owner) internal view returns (uint balance) { unchecked { if (token.eip == 20) { - return IERC20(token.adr).balanceOf(token.recipient); + return IERC20(token.adr).balanceOf(owner); } if (token.eip == 1155) { - return IERC1155(token.adr).balanceOf(token.recipient, token.id); + return IERC1155(token.adr).balanceOf(owner, token.id); } if (token.eip == 721) { if (token.id == EIP_721_ALL) { - return IERC721(token.adr).balanceOf(token.recipient); + return IERC721(token.adr).balanceOf(owner); } - return IERC721(token.adr).ownerOf(token.id) == token.recipient ? 1 : 0; + return IERC721(token.adr).ownerOf(token.id) == owner ? 1 : 0; } if (token.eip == 0) { - return token.recipient.balance; + return owner.balance; } revert("UniversalTokenRouter: INVALID_EIP"); } } - // https://ethereum.stackexchange.com/a/54405 function _sliceUint(bytes memory bs, uint start) internal pure returns (uint x) { unchecked { // require(bs.length >= start + 32, "slicing out of range"); @@ -431,16 +512,13 @@ contract UniversalTokenRouter is IUniversalTokenRouter { } } } - // https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol + /// https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol + /// @param length length of the first preBytes function _concat( bytes memory preBytes, uint length, bytes memory postBytes - ) - internal - pure - returns (bytes memory bothBytes) - { + ) internal pure returns (bytes memory bothBytes) { assembly { // Get a location of some free memory and store it in bothBytes as // Solidity does for memory variables. @@ -510,7 +588,7 @@ contract UniversalTokenRouter is IUniversalTokenRouter { ## Security Considerations -As long as user tokens are only transferred from `msg.sender`, the token allowance can only be used with transactions signed by the token owner. +`LAST_INPUT_RESULT` SHOULD only be used in output action for gas optimization, not as trusted conditions. Application contract code MUST always expect arbitruary, malformed or mallicious `bytes` data can be passed in where the `LAST_INPUT_RESULT` is expected. ## Copyright From 7bf7799eedd19d50fd1ac6c7a2782de906ddcfaf Mon Sep 17 00:00:00 2001 From: Weiji Guo Date: Fri, 6 Jan 2023 14:36:52 +0800 Subject: [PATCH 120/274] Added coauthor (#6267) --- EIPS/eip-5630.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index a604a1c886e1f3..db1ba3b0eed985 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -2,7 +2,7 @@ eip: 5630 title: New approach for encryption / decryption description: defines a specification for encryption and decryption using deterministically derived, pseudorandom keys. -author: Firn Protocol (@firnprotocol), Fried L. Trout +author: Firn Protocol (@firnprotocol), Fried L. Trout, Weiji Guo (@weijiguo) discussions-to: https://ethereum-magicians.org/t/eip-5630-encryption-and-decryption/10761 status: Draft type: Standards Track From 98b75d56b17e71b29af4f982553908910ceb61d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Fri, 6 Jan 2023 10:24:38 -0500 Subject: [PATCH 121/274] Improvements for signature generation (#6270) - Remove EIP-2098 support - Switch `string tokenURI` to more generic `bytes metadata` --- EIPS/eip-4973.md | 73 +++-- .../{ERC-4973.sol => ERC4973-flat.sol} | 301 +++++++++--------- assets/eip-4973/package.json | 45 +++ .../src/index.mjs} | 12 +- .../test/index_test.mjs} | 27 +- 5 files changed, 251 insertions(+), 207 deletions(-) rename assets/eip-4973/{ERC-4973.sol => ERC4973-flat.sol} (87%) create mode 100644 assets/eip-4973/package.json rename assets/eip-4973/{generateSignature.mjs => sdk/src/index.mjs} (50%) rename assets/eip-4973/{generateSignature_test.mjs => sdk/test/index_test.mjs} (56%) diff --git a/EIPS/eip-4973.md b/EIPS/eip-4973.md index 0a0d042b9ee14c..a16ced13212217 100644 --- a/EIPS/eip-4973.md +++ b/EIPS/eip-4973.md @@ -8,7 +8,7 @@ status: Review type: Standards Track category: ERC created: 2022-04-01 -requires: 165, 712, 721, 1271, 2098 +requires: 165, 712, 721, 1271 --- ## Abstract @@ -54,21 +54,20 @@ 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 0x8d7bac72 +/// Note: the ERC-165 identifier for this interface is 0xeb72bb7c 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 /// (`to` == 0). event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId + address indexed from, address indexed to, uint256 indexed tokenId ); /// @notice Count all ABTs assigned to an owner /// @dev ABTs assigned to the zero address are considered invalid, and this /// function throws for queries about the zero address. /// @param owner An address for whom to query the balance /// @return The number of ABTs owned by `address owner`, possibly zero + function balanceOf(address owner) external view returns (uint256); /// @notice Find the address bound to an ERC4973 account-bound token /// @dev ABTs assigned to zero address are considered invalid, and queries @@ -87,11 +86,11 @@ interface IERC4973 { function unequip(uint256 tokenId) external; /// @notice Creates and transfers the ownership of an ABT from the /// transaction's `msg.sender` to `address to`. - /// @dev Throws unless `bytes signature` represents an EIP-2098 Compact - /// Signature of the EIP-712 structured data hash - /// `Agreement(address active,address passive,string tokenURI)` expressing + /// @dev Throws unless `bytes signature` represents a signature of the + // EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` expressing /// `address to`'s explicit agreement to be publicly associated with - /// `msg.sender` and `string tokenURI`. A unique `uint256 tokenId` must be + /// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be /// generated by type-casting the `bytes32` EIP-712 structured data hash to a /// `uint256`. If `bytes signature` is empty or `address to` is a contract, /// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must @@ -99,25 +98,22 @@ interface IERC4973 { /// `event Transfer(msg.sender, to, tokenId)`. Once an ABT exists as an /// `uint256 tokenId` in the contract, `function give(...)` must throw. /// @param to The receiver of the ABT. - /// @param uri A distinct Uniform Resource Identifier (URI) for a given ABT. - /// @param signature A EIP-2098-compatible Compact Signature of the EIP-712 - /// structured data hash - /// `Agreement(address active,address passive,string tokenURI)` signed by + /// @param metadata The metadata that will be associated to the ABT. + /// @param signature A signature of the EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` signed by /// `address to`. /// @return A unique `uint256 tokenId` generated by type-casting the `bytes32` /// EIP-712 structured data hash to a `uint256`. - function give( - address to, - string calldata uri, - bytes calldata signature - ) external returns (uint256); + function give(address to, bytes calldata metadata, bytes calldata signature) + external + returns (uint256); /// @notice Creates and transfers the ownership of an ABT from an /// `address from` to the transaction's `msg.sender`. - /// @dev Throws unless `bytes signature` represents an EIP-2098 Compact - /// Signature of the EIP-712 structured data hash - /// `Agreement(address active,address passive,string tokenURI)` expressing + /// @dev Throws unless `bytes signature` represents a signature of the + /// EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` expressing /// `address from`'s explicit agreement to be publicly associated with - /// `msg.sender` and `string tokenURI`. A unique `uint256 tokenId` must be + /// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be /// generated by type-casting the `bytes32` EIP-712 structured data hash to a /// `uint256`. If `bytes signature` is empty or `address from` is a contract, /// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must @@ -126,26 +122,35 @@ interface IERC4973 { /// exists as an `uint256 tokenId` in the contract, `function take(...)` must /// throw. /// @param from The origin of the ABT. - /// @param uri A distinct Uniform Resource Identifier (URI) for a given ABT. - /// @param signature A EIP-2098-compatible Compact Signature of the EIP-712 - /// structured data hash - /// `Agreement(address active,address passive,string tokenURI)` signed by + /// @param metadata The metadata that will be associated to the ABT. + /// @param signature A signature of the EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` signed by /// `address from`. /// @return A unique `uint256 tokenId` generated by type-casting the `bytes32` /// EIP-712 structured data hash to a `uint256`. - function take( - address from, - string calldata uri, - bytes calldata signature - ) external returns (uint256); + function take(address from, bytes calldata metadata, bytes calldata signature) + external + returns (uint256); + /// @notice Decodes the opaque metadata bytestring of an ABT into the token + /// URI that will be associated with it once it is created on chain. + /// @param metadata The metadata that will be associated to an ABT. + /// @return A URI that represents the metadata. + function decodeURI(bytes calldata metadata) external returns (string memory); } ``` See [EIP-721](./eip-721.md) for a definition of its metadata JSON Schema. -### [EIP-712](./eip-712.md) Typed Structured Data Hashing and [EIP-2098](./eip-2098) Compact Signature Creation +### [EIP-712](./eip-712.md) Typed Structured Data Hashing and Bytearray Signature Creation + +To invoke `function give(...)` and `function take(...)` a bytearray signature must be created using [EIP-712](./eip-712.md). A tested reference implementation in Node.js is attached at [../assets/eip-4973/sdk/src/index.mjs](../assets/eip-4973/sdk/src/index.mjs), [../assets/eip-4973/sdk/test/index_test.mjs](../assets/eip-4973/sdk/test/index_test.mjs) and [../assets/eip-4973/package.json](../assets/eip-4973/package.json). In Solidity, this bytearray signature can be created as follows: -To invoke `function give(...)` and `function take(...)` an [EIP-2098](./eip-2098.md) compact signature must be created using [EIP-712](./eip-712.md). A tested reference implementation is attached at [../assets/eip-4973/generateSignature.mjs](../assets/eip-4973/generateSignature.mjs) and [../assets/eip-4973/generateSignature_test.mjs](../assets/eip-4973/generateSignature_test.mjs). +```solidity +bytes32 r = 0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b90; +bytes32 s = 0x7e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064; +uint8 v = 27; +bytes memory signature = abi.encodePacked(r, s, v); +``` ## Rationale @@ -181,7 +186,7 @@ We have adopted the [EIP-165](./eip-165.md) and `ERC721Metadata` functions purpo ## Reference Implementation -You can find an implementation of this standard in [../assets/eip-4973](../assets/eip-4973/ERC-4973.sol). +You can find an implementation of this standard in [../assets/eip-4973](../assets/eip-4973/ERC4973-flat.sol). ## Security Considerations diff --git a/assets/eip-4973/ERC-4973.sol b/assets/eip-4973/ERC4973-flat.sol similarity index 87% rename from assets/eip-4973/ERC-4973.sol rename to assets/eip-4973/ERC4973-flat.sol index 4b7b60540a3bad..e20aa82208f0ae 100644 --- a/assets/eip-4973/ERC-4973.sol +++ b/assets/eip-4973/ERC4973-flat.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.8; -// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/SignatureChecker.sol) +// OpenZeppelin Contracts (last updated v4.7.1) (utils/cryptography/SignatureChecker.sol) -// OpenZeppelin Contracts (last updated v4.5.0) (utils/cryptography/ECDSA.sol) +// OpenZeppelin Contracts (last updated v4.7.3) (utils/cryptography/ECDSA.sol) -// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol) +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) /** * @dev String operations. @@ -128,9 +128,6 @@ library ECDSA { * _Available since v4.3._ */ function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { - // Check the signature length - // - case 65: r,s,v signature (standard) - // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) _Available since v4.1._ if (signature.length == 65) { bytes32 r; bytes32 s; @@ -144,17 +141,6 @@ library ECDSA { v := byte(0, mload(add(signature, 0x60))) } return tryRecover(hash, v, r, s); - } else if (signature.length == 64) { - bytes32 r; - bytes32 vs; - // ecrecover takes the signature parameters, and the only way to get them - // currently is to use assembly. - /// @solidity memory-safe-assembly - assembly { - r := mload(add(signature, 0x20)) - vs := mload(add(signature, 0x40)) - } - return tryRecover(hash, r, vs); } else { return (address(0), RecoverError.InvalidSignatureLength); } @@ -304,7 +290,7 @@ library ECDSA { } } -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Address.sol) +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol) /** * @dev Collection of functions related to the address type @@ -386,7 +372,7 @@ library Address { * _Available since v3.1._ */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + return functionCall(target, data, "Address: low-level call failed"); } /** @@ -569,7 +555,9 @@ library SignatureChecker { (bool success, bytes memory result) = signer.staticcall( abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) ); - return (success && result.length == 32 && abi.decode(result, (bytes4)) == IERC1271.isValidSignature.selector); + return (success && + result.length == 32 && + abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); } } @@ -673,6 +661,54 @@ abstract contract EIP712 { } } +// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) + +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) + +/** + * @dev Interface of the ERC165 standard, as defined in the + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * Implementers can declare support of contract interfaces, which can then be + * queried by others ({ERC165Checker}). + * + * For an implementation, see {ERC165}. + */ +interface IERC165 { + /** + * @dev Returns true if this contract implements the interface defined by + * `interfaceId`. See the corresponding + * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * to learn more about how these ids are created. + * + * This function call must use less than 30 000 gas. + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} + +/** + * @dev Implementation of the {IERC165} interface. + * + * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check + * for the additional interface id that will be supported. For example: + * + * ```solidity + * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); + * } + * ``` + * + * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. + */ +abstract contract ERC165 is IERC165 { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC165).interfaceId; + } +} + // OpenZeppelin Contracts v4.4.1 (utils/structs/BitMaps.sol) /** @@ -727,54 +763,6 @@ library BitMaps { } } -// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) - -// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) - -/** - * @dev Interface of the ERC165 standard, as defined in the - * https://eips.ethereum.org/EIPS/eip-165[EIP]. - * - * Implementers can declare support of contract interfaces, which can then be - * queried by others ({ERC165Checker}). - * - * For an implementation, see {ERC165}. - */ -interface IERC165 { - /** - * @dev Returns true if this contract implements the interface defined by - * `interfaceId`. See the corresponding - * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] - * to learn more about how these ids are created. - * - * This function call must use less than 30 000 gas. - */ - function supportsInterface(bytes4 interfaceId) external view returns (bool); -} - -/** - * @dev Implementation of the {IERC165} interface. - * - * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check - * for the additional interface id that will be supported. For example: - * - * ```solidity - * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - * return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId); - * } - * ``` - * - * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation. - */ -abstract contract ERC165 is IERC165 { - /** - * @dev See {IERC165-supportsInterface}. - */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IERC165).interfaceId; - } -} - interface IERC721Metadata { function name() external view returns (string memory); function symbol() external view returns (string memory); @@ -783,21 +771,20 @@ interface IERC721Metadata { /// @title Account-bound tokens /// @dev See https://eips.ethereum.org/EIPS/eip-4973 -/// Note: the ERC-165 identifier for this interface is 0x8d7bac72 +/// Note: the ERC-165 identifier for this interface is 0xeb72bb7c 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 /// (`to` == 0). event Transfer( - address indexed from, - address indexed to, - uint256 indexed tokenId + address indexed from, address indexed to, uint256 indexed tokenId ); /// @notice Count all ABTs assigned to an owner /// @dev ABTs assigned to the zero address are considered invalid, and this /// function throws for queries about the zero address. /// @param owner An address for whom to query the balance /// @return The number of ABTs owned by `address owner`, possibly zero + function balanceOf(address owner) external view returns (uint256); /// @notice Find the address bound to an ERC4973 account-bound token /// @dev ABTs assigned to zero address are considered invalid, and queries @@ -816,11 +803,11 @@ interface IERC4973 { function unequip(uint256 tokenId) external; /// @notice Creates and transfers the ownership of an ABT from the /// transaction's `msg.sender` to `address to`. - /// @dev Throws unless `bytes signature` represents an EIP-2098 Compact - /// Signature of the EIP-712 structured data hash - /// `Agreement(address active,address passive,string tokenURI)` expressing + /// @dev Throws unless `bytes signature` represents a signature of the + // EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` expressing /// `address to`'s explicit agreement to be publicly associated with - /// `msg.sender` and `string tokenURI`. A unique `uint256 tokenId` must be + /// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be /// generated by type-casting the `bytes32` EIP-712 structured data hash to a /// `uint256`. If `bytes signature` is empty or `address to` is a contract, /// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must @@ -828,25 +815,22 @@ interface IERC4973 { /// `event Transfer(msg.sender, to, tokenId)`. Once an ABT exists as an /// `uint256 tokenId` in the contract, `function give(...)` must throw. /// @param to The receiver of the ABT. - /// @param uri A distinct Uniform Resource Identifier (URI) for a given ABT. - /// @param signature A EIP-2098-compatible Compact Signature of the EIP-712 - /// structured data hash - /// `Agreement(address active,address passive,string tokenURI)` signed by + /// @param metadata The metadata that will be associated to the ABT. + /// @param signature A signature of the EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` signed by /// `address to`. /// @return A unique `uint256 tokenId` generated by type-casting the `bytes32` /// EIP-712 structured data hash to a `uint256`. - function give( - address to, - string calldata uri, - bytes calldata signature - ) external returns (uint256); + function give(address to, bytes calldata metadata, bytes calldata signature) + external + returns (uint256); /// @notice Creates and transfers the ownership of an ABT from an /// `address from` to the transaction's `msg.sender`. - /// @dev Throws unless `bytes signature` represents an EIP-2098 Compact - /// Signature of the EIP-712 structured data hash - /// `Agreement(address active,address passive,string tokenURI)` expressing + /// @dev Throws unless `bytes signature` represents a signature of the + /// EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` expressing /// `address from`'s explicit agreement to be publicly associated with - /// `msg.sender` and `string tokenURI`. A unique `uint256 tokenId` must be + /// `msg.sender` and `bytes metadata`. A unique `uint256 tokenId` must be /// generated by type-casting the `bytes32` EIP-712 structured data hash to a /// `uint256`. If `bytes signature` is empty or `address from` is a contract, /// an EIP-1271-compatible call to `function isValidSignatureNow(...)` must @@ -855,29 +839,30 @@ interface IERC4973 { /// exists as an `uint256 tokenId` in the contract, `function take(...)` must /// throw. /// @param from The origin of the ABT. - /// @param uri A distinct Uniform Resource Identifier (URI) for a given ABT. - /// @param signature A EIP-2098-compatible Compact Signature of the EIP-712 - /// structured data hash - /// `Agreement(address active,address passive,string tokenURI)` signed by + /// @param metadata The metadata that will be associated to the ABT. + /// @param signature A signature of the EIP-712 structured data hash + /// `Agreement(address active,address passive,bytes metadata)` signed by /// `address from`. /// @return A unique `uint256 tokenId` generated by type-casting the `bytes32` /// EIP-712 structured data hash to a `uint256`. - function take( - address from, - string calldata uri, - bytes calldata signature - ) external returns (uint256); + function take(address from, bytes calldata metadata, bytes calldata signature) + external + returns (uint256); + /// @notice Decodes the opaque metadata bytestring of an ABT into the token + /// URI that will be associated with it once it is created on chain. + /// @param metadata The metadata that will be associated to an ABT. + /// @return A URI that represents the metadata. + function decodeURI(bytes calldata metadata) external returns (string memory); } bytes32 constant AGREEMENT_HASH = - keccak256( - "Agreement(address active,address passive,string tokenURI)" -); + keccak256("Agreement(address active,address passive,bytes metadata)"); /// @notice Reference implementation of EIP-4973 tokens. /// @author Tim Daubenschütz, Rahul Rumalla (https://github.com/rugpullindex/ERC4973/blob/master/src/ERC4973.sol) abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { using BitMaps for BitMaps.BitMap; + BitMaps.BitMap private _usedHashes; string private _name; @@ -887,20 +872,23 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { mapping(uint256 => string) private _tokenURIs; mapping(address => uint256) private _balances; - constructor( - string memory name_, - string memory symbol_, - string memory version - ) EIP712(name_, version) { + constructor(string memory name_, string memory symbol_, string memory version) + EIP712(name_, version) + { _name = name_; _symbol = symbol_; } - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return - interfaceId == type(IERC721Metadata).interfaceId || - interfaceId == type(IERC4973).interfaceId || - super.supportsInterface(interfaceId); + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override + returns (bool) + { + return interfaceId == type(IERC721Metadata).interfaceId + || interfaceId == type(IERC4973).interfaceId + || super.supportsInterface(interfaceId); } function name() public view virtual override returns (string memory) { @@ -911,7 +899,13 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { return _symbol; } - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + function tokenURI(uint256 tokenId) + public + view + virtual + override + returns (string memory) + { require(_exists(tokenId), "tokenURI: token doesn't exist"); return _tokenURIs[tokenId]; } @@ -922,7 +916,13 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { _burn(tokenId); } - function balanceOf(address owner) public view virtual override returns (uint256) { + function balanceOf(address owner) + public + view + virtual + override + returns (uint256) + { require(owner != address(0), "balanceOf: address zero is not a valid owner"); return _balances[owner]; } @@ -933,37 +933,51 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { return owner; } - function give( - address to, - string calldata uri, - bytes calldata signature - ) external virtual returns (uint256) { + function give(address to, bytes calldata metadata, bytes calldata signature) + external + virtual + returns (uint256) + { require(msg.sender != to, "give: cannot give from self"); - uint256 tokenId = _safeCheckAgreement(msg.sender, to, uri, signature); + uint256 tokenId = _safeCheckAgreement(msg.sender, to, metadata, signature); + string memory uri = decodeURI(metadata); _mint(msg.sender, to, tokenId, uri); _usedHashes.set(tokenId); return tokenId; } - function take( - address from, - string calldata uri, - bytes calldata signature - ) external virtual returns (uint256) { + function take(address from, bytes calldata metadata, bytes calldata signature) + external + virtual + returns (uint256) + { require(msg.sender != from, "take: cannot take from self"); - uint256 tokenId = _safeCheckAgreement(msg.sender, from, uri, signature); + uint256 tokenId = _safeCheckAgreement(msg.sender, from, metadata, signature); + string memory uri = decodeURI(metadata); _mint(from, msg.sender, tokenId, uri); _usedHashes.set(tokenId); return tokenId; } + function decodeURI(bytes calldata metadata) + public + virtual + returns (string memory) + { + return string(metadata); + } + function _safeCheckAgreement( address active, address passive, - string calldata uri, + bytes calldata metadata, bytes calldata signature - ) internal virtual returns (uint256) { - bytes32 hash = _getHash(active, passive, uri); + ) + internal + virtual + returns (uint256) + { + bytes32 hash = _getHash(active, passive, metadata); uint256 tokenId = uint256(hash); require( @@ -974,19 +988,13 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { return tokenId; } - function _getHash( - address active, - address passive, - string calldata uri - ) internal view returns (bytes32) { - bytes32 structHash = keccak256( - abi.encode( - AGREEMENT_HASH, - active, - passive, - keccak256(bytes(uri)) - ) - ); + function _getHash(address active, address passive, bytes calldata metadata) + internal + view + returns (bytes32) + { + bytes32 structHash = + keccak256(abi.encode(AGREEMENT_HASH, active, passive, keccak256(metadata))); return _hashTypedDataV4(structHash); } @@ -994,12 +1002,11 @@ abstract contract ERC4973 is EIP712, ERC165, IERC721Metadata, IERC4973 { return _owners[tokenId] != address(0); } - function _mint( - address from, - address to, - uint256 tokenId, - string memory uri - ) internal virtual returns (uint256) { + function _mint(address from, address to, uint256 tokenId, string memory uri) + internal + virtual + returns (uint256) + { require(!_exists(tokenId), "mint: tokenID exists"); _balances[to] += 1; _owners[tokenId] = to; diff --git a/assets/eip-4973/package.json b/assets/eip-4973/package.json new file mode 100644 index 00000000000000..496d41afa50b0f --- /dev/null +++ b/assets/eip-4973/package.json @@ -0,0 +1,45 @@ +{ + "name": "erc4973", + "version": "0.4.0", + "description": "A standard interface for non-transferrable non-fungible tokens, also known as \"account-bound\" or \"soulbound tokens\" or \"badges\".", + "files": [ + "/src/ERC4973.sol", + "/src/ERC165.sol", + "/src/interfaces/IERC165.sol", + "/src/interfaces/IERC4973.sol", + "/src/interfaces/IERC721Metadata.sol", + "/sdk/src/index.mjs" + ], + "scripts": { + "test": "forge test", + "test:sdk": "ava sdk/test", + "gen:flatfile": "forge flatten src/ERC4973.sol > ./assets/ERC4973-flat.sol", + "gen:sdk": "cp package.json assets/package.json && cp -r sdk/ assets/sdk", + "gen:assets": "npm run gen:flatfile && npm run gen:sdk" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rugpullindex/ERC4973.git" + }, + "keywords": [ + "account-bound", + "soulbound", + "tokens", + "ethereum", + "eip", + "badges", + "non-transferrable" + ], + "author": "Tim Daubenschütz (https://timdaub.github.io/)", + "license": "CC0-1.0", + "bugs": { + "url": "https://github.com/rugpullindex/ERC4973/issues" + }, + "homepage": "https://github.com/rugpullindex/ERC4973#readme", + "dependencies": { + "ethers": "5.7.2" + }, + "devDependencies": { + "ava": "4.3.1" + } +} diff --git a/assets/eip-4973/generateSignature.mjs b/assets/eip-4973/sdk/src/index.mjs similarity index 50% rename from assets/eip-4973/generateSignature.mjs rename to assets/eip-4973/sdk/src/index.mjs index b9a9f3eef8e64d..dc94ae324a214c 100644 --- a/assets/eip-4973/generateSignature.mjs +++ b/assets/eip-4973/sdk/src/index.mjs @@ -1,15 +1,9 @@ -// "ethers": "5.6.9" import { utils } from "ethers"; // See: https://docs.ethers.io/v5/api/signer/#Signer-signTypedData for more // detailed instructions. -export async function generateCompactSignature( - signer, - types, - domain, - agreement -) { +export async function generateSignature(signer, types, domain, agreement) { const signature = await signer._signTypedData(domain, types, agreement); - const { compact } = utils.splitSignature(signature); - return compact; + const { r, s, v } = utils.splitSignature(signature); + return utils.solidityPack(["bytes32", "bytes32", "uint8"], [r, s, v]); } diff --git a/assets/eip-4973/generateSignature_test.mjs b/assets/eip-4973/sdk/test/index_test.mjs similarity index 56% rename from assets/eip-4973/generateSignature_test.mjs rename to assets/eip-4973/sdk/test/index_test.mjs index eb110676a125ac..2adf1fc7a12fc2 100644 --- a/assets/eip-4973/generateSignature_test.mjs +++ b/assets/eip-4973/sdk/test/index_test.mjs @@ -1,11 +1,9 @@ // @format -// "ava": "4.3.1" import test from "ava"; -// "ethers": "5.6.9" -import { Wallet } from "ethers"; +import { Wallet, utils } from "ethers"; -import { generateCompactSignature } from "../src/index.mjs"; +import { generateSignature } from "../src/index.mjs"; test("generating a compact signature for function give", async (t) => { // from: https://docs.ethers.io/v5/api/signer/#Wallet--methods @@ -19,7 +17,7 @@ test("generating a compact signature for function give", async (t) => { Agreement: [ { name: "active", type: "address" }, { name: "passive", type: "address" }, - { name: "tokenURI", type: "string" }, + { name: "metadata", type: "bytes" }, ], }; const domain = { @@ -32,19 +30,14 @@ test("generating a compact signature for function give", async (t) => { const agreement = { active: "0xb4c79dab8f259c7aee6e5b2aa729821864227e84", passive: passiveAddress, - tokenURI: "https://contenthash.com", + metadata: utils.toUtf8Bytes("https://example.com/metadata.json"), }; - const compactSignature = await generateCompactSignature( - signer, - types, - domain, - agreement - ); - t.truthy(compactSignature); - // For length of compact signature, see https://eips.ethereum.org/EIPS/eip-2098#backwards-compatibility - t.is(compactSignature.length, 64 * 2 + 2); + + const signature = await generateSignature(signer, types, domain, agreement); + t.truthy(signature); + t.is(signature.length, 64 + 64 + 2 + 2); t.is( - compactSignature, - "0x238e1616c507f9779469b0276eef73a3a438b65706ca18c6ab38062c588674f9719c9f5412b0379e7918f19da1de71b9370ed9917fadcb6690e71f5a1de24816" + signature, + "0x4473afdec84287f10aa0b5eb608d360e2e9220bee657a4a5ca468e69a4de255c38691fca0c52f295d1831beaa0b7f079c1ab7959257578d2fb8d98740d9b0e111c" ); }); From e6a2a4adea36283d847bcaca9c1aa672553af769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20Daubensch=C3=BCtz?= Date: Fri, 6 Jan 2023 10:33:24 -0500 Subject: [PATCH 122/274] Clarify document ownership (#6271) --- EIPS/eip-4973.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4973.md b/EIPS/eip-4973.md index a16ced13212217..7a8f4e89cc1b9b 100644 --- a/EIPS/eip-4973.md +++ b/EIPS/eip-4973.md @@ -2,7 +2,7 @@ eip: 4973 title: Account-bound Tokens description: An interface for non-transferrable NFTs binding to an Ethereum account like a legendary World of Warcraft item binds to a character. -author: Tim Daubenschütz (@TimDaub), Raphael Roullet (@ra-phael) +author: Tim Daubenschütz (@TimDaub) discussions-to: https://ethereum-magicians.org/t/eip-4973-non-transferrable-non-fungible-tokens-soulbound-tokens-or-badges/8825 status: Review type: Standards Track From 6f89fdd006e00ac575546eb189ccb79d50e80f1c Mon Sep 17 00:00:00 2001 From: lightclient <14004106+lightclient@users.noreply.github.com> Date: Fri, 6 Jan 2023 10:31:07 -0700 Subject: [PATCH 123/274] Update EIP-1459: unstagnate and add security consideration (#6265) * 1459: unstagnate and add security consideration * appease our overlord, master walidator --- EIPS/eip-1459.md | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-1459.md b/EIPS/eip-1459.md index 00416b0dc86487..b59df0eddbd370 100644 --- a/EIPS/eip-1459.md +++ b/EIPS/eip-1459.md @@ -3,12 +3,12 @@ eip: 1459 title: Node Discovery via DNS description: Scheme for authenticated updateable Ethereum node lists via DNS. author: Felix Lange (@fjl), Péter Szilágyi (@karalabe) +discussions-to: https://github.com/ethereum/devp2p/issues/50 +status: Draft type: Standards Track category: Networking -status: Stagnant created: 2018-09-26 requires: 778 -discussions-to: https://github.com/ethereum/devp2p/issues/50 --- ## Abstract @@ -90,15 +90,13 @@ packets. This limits the number of hashes that can be placed into an Example in zone file format: -```text -; name ttl class type content -@ 60 IN TXT enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA -C7HRFPF3BLGF3YR4DY5KX3SMBE 86900 IN TXT enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org -JWXYDBPXYWG6FX3GMDIBFA6CJ4 86900 IN TXT enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24 -2XS2367YHAXJFGLZHVAWLQD4ZY 86900 IN TXT enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA -H4FHT4B454P6UXFD7JCYQ5PWDY 86900 IN TXT enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI -MHTDO6TMUBRIA2XWG5LUDACK24 86900 IN TXT enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o -``` + ; name ttl class type content + @ 60 IN TXT enrtree-root:v1 e=JWXYDBPXYWG6FX3GMDIBFA6CJ4 l=C7HRFPF3BLGF3YR4DY5KX3SMBE seq=1 sig=o908WmNp7LibOfPsr4btQwatZJ5URBr2ZAuxvK4UWHlsB9sUOTJQaGAlLPVAhM__XJesCHxLISo94z5Z2a463gA + C7HRFPF3BLGF3YR4DY5KX3SMBE 86900 IN TXT enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org + JWXYDBPXYWG6FX3GMDIBFA6CJ4 86900 IN TXT enrtree-branch:2XS2367YHAXJFGLZHVAWLQD4ZY,H4FHT4B454P6UXFD7JCYQ5PWDY,MHTDO6TMUBRIA2XWG5LUDACK24 + 2XS2367YHAXJFGLZHVAWLQD4ZY 86900 IN TXT enr:-HW4QOFzoVLaFJnNhbgMoDXPnOvcdVuj7pDpqRvh6BRDO68aVi5ZcjB3vzQRZH2IcLBGHzo8uUN3snqmgTiE56CH3AMBgmlkgnY0iXNlY3AyNTZrMaECC2_24YYkYHEgdzxlSNKQEnHhuNAbNlMlWJxrJxbAFvA + H4FHT4B454P6UXFD7JCYQ5PWDY 86900 IN TXT enr:-HW4QAggRauloj2SDLtIHN1XBkvhFZ1vtf1raYQp9TBW2RD5EEawDzbtSmlXUfnaHcvwOizhVYLtr7e6vw7NAf6mTuoCgmlkgnY0iXNlY3AyNTZrMaECjrXI8TLNXU0f8cthpAMxEshUyQlK-AM0PW2wfrnacNI + MHTDO6TMUBRIA2XWG5LUDACK24 86900 IN TXT enr:-HW4QLAYqmrwllBEnzWWs7I5Ev2IAs7x_dZlbYdRdMUx5EyKHDXp7AV5CkuPGUPdvbv1_Ms1CPfhcGCvSElSosZmyoqAgmlkgnY0iXNlY3AyNTZrMaECriawHKWdDRk2xeZkrOXBQ0dfMFLHY4eENZwdufn1S1o ### Client Protocol @@ -163,6 +161,12 @@ enable client implementations to sync these trees independently. A client wanting to get as many nodes as possible will sync the link tree first and add all linked names to the sync horizon. +## Security Considerations + +Discovery via DNS is less secure than via DHT, because it relies on a trusted +party to publish the records regularly. The actor could easily eclipse +bootstrapping nodes by only publishing node records that it controls. + ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From 3f3f99e447a5a1c6cb8678fd5dad2c85a9dd94bd Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:31:34 -0500 Subject: [PATCH 124/274] CI: Only run label bot on PRs to master (#6255) --- .github/workflows/auto-label-bot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/auto-label-bot.yml b/.github/workflows/auto-label-bot.yml index d4edd8fdb50bb5..aded93281f751b 100644 --- a/.github/workflows/auto-label-bot.yml +++ b/.github/workflows/auto-label-bot.yml @@ -1,5 +1,7 @@ on: pull_request_target: + branches: + - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From e3c784a40563893c7d9eb573aeb4a9bd6182b738 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:33:04 -0500 Subject: [PATCH 125/274] Update EIP-3267: Fix SPDX license identifier (#5227) * Make EIP-3267 assets CC0 * I guess I'll fix a typo to allow EIP bot to merge this with approval --- EIPS/eip-3267.md | 2 +- assets/eip-3267/contracts/BaseBidOnAddresses.sol | 2 +- assets/eip-3267/contracts/BaseLock.sol | 2 +- assets/eip-3267/contracts/BaseRestorableSalary.sol | 2 +- assets/eip-3267/contracts/BaseSalary.sol | 2 +- assets/eip-3267/contracts/BidOnAddresses.sol | 2 +- assets/eip-3267/contracts/DAOInterface.sol | 2 +- assets/eip-3267/contracts/DefaultDAOInterface.sol | 2 +- assets/eip-3267/contracts/ERC1155/ERC1155.sol | 2 +- assets/eip-3267/contracts/ERC1155/ERC1155TokenReceiver.sol | 2 +- assets/eip-3267/contracts/ERC1155/ERC1155WithTotals.sol | 2 +- assets/eip-3267/contracts/ERC1155/IERC1155.sol | 2 +- assets/eip-3267/contracts/ERC1155/IERC1155TokenReceiver.sol | 2 +- assets/eip-3267/contracts/Salary.sol | 2 +- assets/eip-3267/contracts/SalaryWithDAO.sol | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/EIPS/eip-3267.md b/EIPS/eip-3267.md index 8363e0719ac84f..c44fe3e481a54a 100644 --- a/EIPS/eip-3267.md +++ b/EIPS/eip-3267.md @@ -37,7 +37,7 @@ Paradoxically, it will directly benefit miners/validators, see the discussion. `MineFraction` = `TBD` (0..1) -[The contracts source](../assets/eip-3267/contracts/README.md) +[The contract's source](../assets/eip-3267/contracts/README.md) Prior to `FORK_BLOCK_NUMBER`, `SalaryWithDAO` and `DefaultDAOInterface` contracts will be deployed to the network and exist at the above specified addresses. diff --git a/assets/eip-3267/contracts/BaseBidOnAddresses.sol b/assets/eip-3267/contracts/BaseBidOnAddresses.sol index f39deebf7c8567..ca693462ab989e 100644 --- a/assets/eip-3267/contracts/BaseBidOnAddresses.sol +++ b/assets/eip-3267/contracts/BaseBidOnAddresses.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { ABDKMath64x64 } from "abdk-libraries-solidity/ABDKMath64x64.sol"; diff --git a/assets/eip-3267/contracts/BaseLock.sol b/assets/eip-3267/contracts/BaseLock.sol index ca661791fab447..7e45627d436743 100644 --- a/assets/eip-3267/contracts/BaseLock.sol +++ b/assets/eip-3267/contracts/BaseLock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import { SafeMath } from "@openzeppelin/contracts/math/SafeMath.sol"; import { ABDKMath64x64 } from "abdk-libraries-solidity/ABDKMath64x64.sol"; diff --git a/assets/eip-3267/contracts/BaseRestorableSalary.sol b/assets/eip-3267/contracts/BaseRestorableSalary.sol index 2916b6c8705740..d7eac2da5cc0ea 100644 --- a/assets/eip-3267/contracts/BaseRestorableSalary.sol +++ b/assets/eip-3267/contracts/BaseRestorableSalary.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./Salary.sol"; diff --git a/assets/eip-3267/contracts/BaseSalary.sol b/assets/eip-3267/contracts/BaseSalary.sol index 276b97369f2de3..a695f5353baee6 100644 --- a/assets/eip-3267/contracts/BaseSalary.sol +++ b/assets/eip-3267/contracts/BaseSalary.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./BaseBidOnAddresses.sol"; diff --git a/assets/eip-3267/contracts/BidOnAddresses.sol b/assets/eip-3267/contracts/BidOnAddresses.sol index baeb637c45cb09..ec41e778029dba 100644 --- a/assets/eip-3267/contracts/BidOnAddresses.sol +++ b/assets/eip-3267/contracts/BidOnAddresses.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./BaseBidOnAddresses.sol"; diff --git a/assets/eip-3267/contracts/DAOInterface.sol b/assets/eip-3267/contracts/DAOInterface.sol index 86417857ced575..c2f15efa833da0 100644 --- a/assets/eip-3267/contracts/DAOInterface.sol +++ b/assets/eip-3267/contracts/DAOInterface.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; /// @notice The "DAO plugin" interface. diff --git a/assets/eip-3267/contracts/DefaultDAOInterface.sol b/assets/eip-3267/contracts/DefaultDAOInterface.sol index 79cf6838de89b8..15514550e45f53 100644 --- a/assets/eip-3267/contracts/DefaultDAOInterface.sol +++ b/assets/eip-3267/contracts/DefaultDAOInterface.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./DAOInterface.sol"; diff --git a/assets/eip-3267/contracts/ERC1155/ERC1155.sol b/assets/eip-3267/contracts/ERC1155/ERC1155.sol index de4637f9b78fa1..d7f2af5b9a6ed1 100644 --- a/assets/eip-3267/contracts/ERC1155/ERC1155.sol +++ b/assets/eip-3267/contracts/ERC1155/ERC1155.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity >=0.6.0 <0.8.0; diff --git a/assets/eip-3267/contracts/ERC1155/ERC1155TokenReceiver.sol b/assets/eip-3267/contracts/ERC1155/ERC1155TokenReceiver.sol index 7e888ba6599b6d..9832abca30cb2d 100644 --- a/assets/eip-3267/contracts/ERC1155/ERC1155TokenReceiver.sol +++ b/assets/eip-3267/contracts/ERC1155/ERC1155TokenReceiver.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./IERC1155TokenReceiver.sol"; diff --git a/assets/eip-3267/contracts/ERC1155/ERC1155WithTotals.sol b/assets/eip-3267/contracts/ERC1155/ERC1155WithTotals.sol index 95bf2285ced6a5..407356f7c6c33d 100644 --- a/assets/eip-3267/contracts/ERC1155/ERC1155WithTotals.sol +++ b/assets/eip-3267/contracts/ERC1155/ERC1155WithTotals.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "@openzeppelin/contracts/math/SafeMath.sol"; import { ERC1155 } from "./ERC1155.sol"; diff --git a/assets/eip-3267/contracts/ERC1155/IERC1155.sol b/assets/eip-3267/contracts/ERC1155/IERC1155.sol index 2ecf0f1821e23d..e763657c8e14d1 100644 --- a/assets/eip-3267/contracts/ERC1155/IERC1155.sol +++ b/assets/eip-3267/contracts/ERC1155/IERC1155.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "@openzeppelin/contracts/introspection/IERC165.sol"; diff --git a/assets/eip-3267/contracts/ERC1155/IERC1155TokenReceiver.sol b/assets/eip-3267/contracts/ERC1155/IERC1155TokenReceiver.sol index f737f41c7104a6..044260ad747cd5 100644 --- a/assets/eip-3267/contracts/ERC1155/IERC1155TokenReceiver.sol +++ b/assets/eip-3267/contracts/ERC1155/IERC1155TokenReceiver.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "@openzeppelin/contracts/introspection/IERC165.sol"; diff --git a/assets/eip-3267/contracts/Salary.sol b/assets/eip-3267/contracts/Salary.sol index 5cbc23d57d4389..83d48086d50a61 100644 --- a/assets/eip-3267/contracts/Salary.sol +++ b/assets/eip-3267/contracts/Salary.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import "./BaseSalary.sol"; diff --git a/assets/eip-3267/contracts/SalaryWithDAO.sol b/assets/eip-3267/contracts/SalaryWithDAO.sol index 6b6fc23172669c..cbe21106d9ff41 100644 --- a/assets/eip-3267/contracts/SalaryWithDAO.sol +++ b/assets/eip-3267/contracts/SalaryWithDAO.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LGPL-3.0-or-later +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.7.1; import { ABDKMath64x64 } from "abdk-libraries-solidity/ABDKMath64x64.sol"; import "./BaseRestorableSalary.sol"; From 56c518715225e914c2e8d5a88ed2c1825ba2c72d Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 6 Jan 2023 19:43:35 +0100 Subject: [PATCH 126/274] Add EIP-5805: Voting with delegation (#5805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial draft * add eip number * rename focument * add discussion * fix EIP Walidator checks * Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * fix typo * rename now → clock * fix markdown linter Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5805.md | 406 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 EIPS/eip-5805.md diff --git a/EIPS/eip-5805.md b/EIPS/eip-5805.md new file mode 100644 index 00000000000000..657216340d39d7 --- /dev/null +++ b/EIPS/eip-5805.md @@ -0,0 +1,406 @@ +--- +eip: 5805 +title: Voting with delegation +description: An interface for voting weight tracking, with delegation support +author: Hadrien Croubois (@Amxx) +discussions-to: https://ethereum-magicians.org/t/eip-5805-voting-with-delegation/11407 +status: Draft +type: Standards Track +category: ERC +created: 2022-07-04 +requires: 712 +--- + +## Abstract + +Many DAOs (decentralized autonomous organizations) rely on tokens to represent one's voting power. In order to perform this task effectively, the token contracts need to include specific mechanisms such as checkpoints and delegation. The existing implementations are not standardized. This EIP proposes to standardize the way votes are delegated from one account to another, and the way current and past votes are tracked and queried. The corresponding behavior is compatible with many token types, including but not limited to [EIP-20](./eip-20.md) and [EIP-721](./eip-721.md). This EIP also considers the diversity of time tracking functions, allowing the voting tokens (and any contract associated with it) to track the votes based on `block.number`, `block.timestamp`, or any other non-decreasing function. + +## Motivation + +Beyond simple monetary transactions, decentralized autonomous organizations are arguably one of the most important use cases of blockchain and smart contract technologies. Today, many communities are organized around a governance contract that allows users to vote. Among these communities, some represent voting power using transferable tokens ([EIP-20](./eip-20.md), [EIP-721](./eip-721.md), other). In this context, the more tokens one owns, the more voting power one has. Governor contracts, such as Compound's `GovernorBravo`, read from these "voting token" contracts to get the voting power of the users. + +Unfortunately, simply using the `balanceOf(address)` function present in most token standards is not good enough: + +- The values are not checkpointed, so a user can vote, transfer its tokens to a new account, and vote again with the same tokens. +- A user cannot delegate their voting power to someone else without transferring full ownership of the tokens. + +These constraints have led to the emergence of voting tokens with delegation that the following logic: + +- Users can delegate the voting power of their tokens to themselves or a third party. This creates a distinction between balance and voting weight. +- The voting weights of accounts are checkpointed, allowing lookups for past values at different points in time. +- The balances are not checkpointed. + +This EIP is proposing to standardize the interface and behavior of these voting tokens. + +Additionally, the existing (non-standardized) implementations are limited to `block.number` based checkpoints. This choice causes many issues in a multichain environment, where some chains (particularly L2s) have an inconsistent or unpredictable time between blocks. This EIP also addresses this issue by allowing the voting token to use any time tracking function it wants, and exposing it so that other contracts (such as a Governor) can stay consistent with the token checkpoints. + +## Specification + +Following pre-existing (but not-standardized) implementation, the EIP proposes the following mechanism. + +Each user account (address) can delegate to an account of its choice. This can be itself, someone else, or no one (represented by `address(0)`). Assets held by the user cannot express their voting power unless they are delegated. + +When a "delegator" delegates its tokens voting power to a "delegatee", its balance is added to the voting power of the delegatee. If the delegator changes its delegation, the voting power is subtracted from the old delegatee's voting power and added to the new delegate's voting power. The voting power of each account is tracked through time so that it is possible to query its value in the past. With tokens being delegated to at most one delegate at a given point in time, double voting is prevented. + +Whenever tokens are transferred from one account to another, the associated voting power should be deducted from the sender's delegate and added to the receiver's delegate. + +Tokens that are delegated to `address(0)` should not be tracked. This allows users to optimize the gas cost of their token transfers by skipping the checkpoint update for their delegate. + +To accommodate different types of chains, we want the voting checkpoint system to support different forms of time tracking. On the Ethereum mainnet, using block numbers provides backward compatibility with applications that historically use it. On the other hand, using timestamps provides better semantics for end users, and accommodates use cases where the duration is expressed in seconds. Other monotonic functions could also be deemed relevant by developers based on the characteristics of future applications and blockchains. + +Both timestamps, block numbers, and other possible modes use the same external interfaces. This allows transparent binding of third-party contracts, such as governor systems, to the vote tracking built into the voting contracts. For this to be effective, the voting contracts must, in addition to all the vote-tracking functions, expose the current value used for time-tracking. + +### Methods + +#### clock + +This function returns the current timepoint. It could be `block.timestamp`, `block.number` (or any other **non-decreasing** function) depending on the mode the contract is operating on. + +- If operating using **block number**, then this function SHOULD be implemented. +- If operating using **timestamp**, then this function MUST be implemented. +- If operating using any other mode, then this function MUST be implemented. + +This function is thus optional, and its absence should be considered as a marker of the contract operating using block number. (This makes this EIP compatible with pre-existing voting contracts) + +```yaml +- name: clock + type: function + stateMutability: view + inputs: [] + outputs: + - name: timepoint + type: uint256 +``` + +#### getVotes + +This function returns the current voting weight of an account. This corresponds to all the voting power delegated to it at the moment this function is called. + +As tokens delegated to `address(0)` should not be counted/snapshoted, `getVotes(0)` SHOULD always return `0`. + +This function MUST be implemented + +```yaml +- name: getVotes + type: function + stateMutability: view + inputs: + - name: account + type: address + outputs: + - name: votingWeight + type: uint256 +``` + +#### getPastVotes + +This function returns the historical voting weight of an account. This corresponds to all the voting power delegated to it at a specific timepoint. The timepoint parameter should match the operating mode of the contract. This function SHOULD only serve past checkpoints that are immutable. Calling this function with a timepoint that is greater or equal to `clock()` SHOULD revert. For any timepoint that is strictly smaller than `clock()`, the value returned by `getPastVotes` should be constant. + +As tokens delegated to `address(0)` should not be counted/snapshoted, `getPastVotes(0,x)` SHOULD always return `0` (for all values of `x`). + +This function MUST be implemented + +```yaml +- name: getPastVotes + type: function + stateMutability: view + inputs: + - name: account + type: address + - name: timepoint + type: uint256 + outputs: + - name: votingWeight + type: uint256 +``` + +#### delegates + +This function returns the address to which the voting power of an account is currently delegated. + +Note that if the delegate is `address(0)` then the voting power SHOULD NOT be checkpointed, and it should not be possible to vote with it. + +This function MUST be implemented + +```yaml +- name: delegates + type: function + stateMutability: view + inputs: + - name: account + type: address + outputs: + - name: delegatee + type: address +``` + +#### delegate + +This function changes the caller's delegate, updating the vote delegation in the meantime. + +This function MUST be implemented + +```yaml +- name: delegate + type: function + stateMutability: nonpayable + inputs: + - name: delegatee + type: address + outputs: [] +``` + +#### delegateBySig + +This function changes an account's delegate using a signature, updating the vote delegation in the meantime. + +This function MUST be implemented + +```yaml +- name: delegateBySig + type: function + stateMutability: nonpayable + inputs: + - name: delegatee + type: address + - name: nonce + type: uint256 + - name: expiry + type: uint256 + - name: v + type: uint8 + - name: r + type: bytes32 + - name: s + type: bytes32 + outputs: [] +``` + +This signature should follow the [EIP-712](./eip-712.md) format: + +A call to `delegateBySig(delegatee, nonce, expiry, v, r, s)` changes the signer's delegate to `delegatee`, increment the signer's nonce by 1, and emits a corresponding `DelegateChanged` event, and possibly `DelegateVotesChanged` events for the old and the new delegate accounts, if and only if the following conditions are met: + + +- The current timestamp is less than or equal to `expiry`. +- `nonces(signer)` (before the state update) is equal to `nonce`. + +If any of these conditions are not met, the `delegateBySig` call must revert. This translate to the following solidity code: + +```sol +require(expiry <= block.timestamp) +bytes signer = ecrecover( + keccak256(abi.encodePacked( + hex"1901", + DOMAIN_SEPARATOR, + keccak256(abi.encode( + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"), + delegatee, + nonce, + expiry)), + v, r, s) +require(signer != address(0)); +require(nounces[signer] == nonce); +// increment nonce +// set delegation of `signer` to `delegatee` +``` + +where `DOMAIN_SEPARATOR` is defined according to [EIP-712](./eip-712.md). The `DOMAIN_SEPARATOR` should be unique to the contract and chain to prevent replay attacks from other domains, +and satisfy the requirements of EIP-712, but is otherwise unconstrained. + +A common choice for `DOMAIN_SEPARATOR` is: + +```solidity +DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'), + keccak256(bytes(name)), + keccak256(bytes(version)), + chainid, + address(this) +)); +``` + +In other words, the message is the EIP-712 typed structure: + +```js +{ + "types": { + "EIP712Domain": [ + { + "name": "name", + "type": "string" + }, + { + "name": "version", + "type": "string" + }, + { + "name": "chainId", + "type": "uint256" + }, + { + "name": "verifyingContract", + "type": "address" + } + ], + "Delegation": [{ + "name": "delegatee", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "expiry", + "type": "uint256" + } + ], + "primaryType": "Permit", + "domain": { + "name": contractName, + "version": version, + "chainId": chainid, + "verifyingContract": contractAddress + }, + "message": { + "delegatee": delegatee, + "nonce": nonce, + "expiry": expiry + } +}} +``` + +Note that nowhere in this definition do we refer to `msg.sender`. The caller of the `delegateBySig` function can be any address. + +When this function is successfully executed, the delegator's nonce MUST be incremented to prevent replay attacks. + +#### nonces + +This function returns the current nonce for a given account. + +Signed delegations (see `delegateBySig`) are only accepted if the nonce used in the EIP-712 signature matches the return of this function. This value of `nonce(delegator)` should be incremented whenever a call to `delegateBySig` is performed on behalf of `delegator`. + +This function MUST be implemented + +```yaml +- name: nonces + type: function + stateMutability: view + inputs: + - name: account + type: delegator + outputs: + - name: nonce + type: uint256 +``` + +### Events + +#### DelegateChanged + +`delegator` changes the delegation of its assets from `fromDelegate` to `toDelegate`. + +MUST be emitted when the delegate for an account is modified by `delegate(address)` or `delegateBySig(address,uint256,uint256,uint8,bytes32,bytes32)`. + +```yaml +- name: DelegateChanged + type: event + inputs: + - name: delegator + indexed: true + type: address + - name: fromDelegate + indexed: true + type: address + - name: toDelegate + indexed: true + type: address +``` + +#### DelegateVotesChanged + +`delegate` available voting power changes from `previousBalance` to `newBalance`. + +This MUST be emitted when: + +- an account (that holds more than 0 assets) updates its delegation from or to `delegate`, +- an asset transfer from or to an account that is delegated to `delegate`. + +```yaml +- name: DelegateVotesChanged + type: event + inputs: + - name: delegate + indexed: true + type: address + - name: previousBalance + indexed: false + type: uint256 + - name: newBalance + indexed: false + type: uint256 +``` + +### Solidity interface + +```sol +interface IERC_XXXX { + event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); + event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); + + function clock() external view returns (uint256); + function getVotes(address account) external view returns (uint256); + function getPastVotes(address account, uint256 timepoint) external view returns (uint256); + function delegates(address account) external view returns (address); + function nonces(address owner) public view virtual returns (uint256) + + function delegate(address delegatee) external; + function delegateBySig(address delegatee, uint256 nonce, uint256 expiry, uint8 v, bytes32 r, bytes32 s) external; +} +``` + +### Expected properties + +- The `clock()` function MUST be non-decreasing. +- For all timepoints `t < clock()`, `getVotes(address(0))` and `getPastVotes(address(0), t)` SHOULD return 0. +- For all accounts `a != 0`, `getVotes(a)` SHOULD be the sum of the "balances" of all the accounts that delegate to `a`. +- For all accounts `a != 0` and all timestamp `t < clock()`, `getPastVotes(a, t)` SHOULD be the sum of the "balances" of all the accounts that delegated to `a` when `clock()` overtook `t`. +- For all accounts `a`, `getPastVotes(a, t)` MUST be constant after `t < clock()` is reached. +- For all accounts `a`, the action of changing the delegate from `b` to `c` MUST not increase the current voting power of `b` (`getVotes(b)`) and MUST not decrease the current voting power of `c` (`getVotes(c)`). + +## Rationale + +Delegation allows token holders to trust a delegate with their vote while keeping full custody of their token. This means that only a small-ish number of delegates need to pay gas for voting. This leads to better representation of small token holders by allowing their votes to be cast without requiring them to pay expensive gas fees. Users can take over their voting power at any point, and delegate it to someone else, or to themselves. + +The use of checkpoints prevents double voting. Votes, for example in the context of a governance proposal, should rely on a snapshot defined by a timepoint. Only tokens delegated at that timepoint can be used for voting. This means any token transfer performed after the snapshot will not affect the voting power of the sender/receiver's delegate. This also means that in order to vote, someone must acquire tokens and delegate them before the snapshot is taken. Governors can, and do, include a delay between the proposal is submitted and the snapshot is taken so that users can take the necessary actions (change their delegation, buy more tokens, ...). + +`delegateBySig` is necessary to offer a gasless workflow to token holders that do not want to pay gas for voting. + +The `nonces` mapping is given for replay protection. + +EIP-712 typed messages are included because of its widespread adoption in many wallet providers. + +## Backwards Compatibility + +Compound and OpenZeppelin already provide implementations of voting tokens. The delegation-related methods are shared between the two implementations and this EIP. For the vote lookup, this EIP uses OpenZeppelin's implementation (with return type uint256) as Compound's implementation causes significant restrictions of the acceptable values (return type is uint96). + +Both implementations use `block.number` for their checkpoints and do not implement the `clock()` method, which is compatible with this EIP. + +Existing governors, that are currently compatible with OpenZeppelin's implementation will be compatible with the "block number mode" of this EIP. + +## Security Considerations + +Before doing a lookup, one should check the return value of `clock()` and make sure that the parameters of the lookup are consistent. Performing a lookup using a timestamp argument on a contract that uses block numbers will very likely cause a revert. On the other end, performing a lookup using a block number argument on a contract that uses timestamps will likely return 0. + +Though the signer of a `Delegation` may have a certain party in mind to submit their transaction, another party can always front-run this transaction and call `delegateBySig` before the intended party. The result is the same for the `Delegation` signer, however. + +Since the ecrecover precompile fails silently and just returns the zero address as `signer` when given malformed messages, it is important to ensure `signer != address(0)` to avoid `delegateBySig` from delegating "zombie funds" belonging to the zero address. + +Signed `Delegation` messages are censorable. The relaying party can always choose to not submit the `Delegation` after having received it, withholding the option to submit it. The `expiry` parameter is one mitigation to this. If the signing party holds ETH they can also just submit the `Delegation` themselves, which can render previously signed `Delegation`s invalid. + +If the `DOMAIN_SEPARATOR` contains the `chainId` and is defined at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From e21db665baa287b0b6e67baf9d4f593725f96d99 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 14:06:31 -0500 Subject: [PATCH 127/274] Update EIP-2771: Fix markdownlint issues (#6274) --- EIPS/eip-2771.md | 1 + 1 file changed, 1 insertion(+) diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index 117c47304a25e2..efcd7e61fa2c2a 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -95,6 +95,7 @@ Internally, the **Recipient** MUST then accept a request from forwarder. 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 From da1957f1b04001cc11a94d3c0c6bd8f5ce168e4d Mon Sep 17 00:00:00 2001 From: Suriyaa Sundararuban Date: Fri, 6 Jan 2023 20:35:23 +0100 Subject: [PATCH 128/274] Update GitHub docs link (#6213) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4135b539e6eee..dbf8c9d68528c5 100644 --- a/README.md +++ b/README.md @@ -78,4 +78,4 @@ eipv 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://docs.github.com/en/enterprise/2.14/user/articles/setting-up-your-github-pages-site-locally-with-jekyll). From 96e1162f422b6da79f7af9b9a63b95e4f08d4a61 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 14:38:42 -0500 Subject: [PATCH 129/274] Update CI: Fix EIP bot sometimes not triggering (#6275) --- .github/workflows/auto-review-bot.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/auto-review-bot.yml b/.github/workflows/auto-review-bot.yml index e0dd983eea6f07..696694c700aa17 100644 --- a/.github/workflows/auto-review-bot.yml +++ b/.github/workflows/auto-review-bot.yml @@ -5,10 +5,6 @@ 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: From e50c6873f9d7c25865a57882f38eab38dcde9564 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 14:40:08 -0500 Subject: [PATCH 130/274] Update CI: Fix automatic messaging and labeling (#6276) There were a few typos/other errors --- .github/workflows/post-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/post-ci.yml b/.github/workflows/post-ci.yml index 1198d47c66d2b7..b6243f0292ca57 100644 --- a/.github/workflows/post-ci.yml +++ b/.github/workflows/post-ci.yml @@ -31,6 +31,7 @@ jobs: uses: marocchino/sticky-pull-request-comment@39c5b5dc7717447d0cba270cd115037d32d28443 if: ${{ github.event.workflow_run.conclusion == 'failure' }} with: + number: ${{ steps.save-pr-data.outputs.pr_number }} recreate: true message: | The commit ${{ steps.save-pr-data.outputs.pr_sha }} (as a parent of ${{ steps.save-pr-data.outputs.merge_sha }}) contains errors. @@ -41,8 +42,9 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'failure' }} with: labels: w-ci - number: ${{ steps.save-pr-data.outputs.pr_number }}' + number: ${{ steps.save-pr-data.outputs.pr_number }} repo: ${{ github.repository }} + github_token: ${{ github.token }} - name: Remove Waiting Label uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 @@ -51,3 +53,4 @@ jobs: labels: w-ci number: ${{ steps.save-pr-data.outputs.pr_number }} repo: ${{ github.repository }} + github_token: ${{ github.token }} From d8e492c69309d532234b78da23b817055dceceb3 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 15:12:42 -0500 Subject: [PATCH 131/274] Update EIP-2771: Move to Last Call (#6264) --- EIPS/eip-2771.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index efcd7e61fa2c2a..75f3f2fbfd6151 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -4,7 +4,8 @@ title: Secure Protocol for Native Meta Transactions 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: Review +status: Last Call +last-call-deadline: 2023-01-20 type: Standards Track category: ERC created: 2020-07-01 From cc3c7677d8fd33c4b95e6ab8620ca0333da88ac1 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 15:28:07 -0500 Subject: [PATCH 132/274] Update EIP-2771: Fix typo (#6281) --- EIPS/eip-2771.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index 75f3f2fbfd6151..1c008c157547bd 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -75,7 +75,7 @@ function isTrustedForwarder(address forwarder) external view returns(bool); 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. +`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. ## Rationale From 23c08bfb8cbf4855e402d35de75c7d9f951f8703 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 15:46:58 -0500 Subject: [PATCH 133/274] Update EIP-5507: Fix file names (#6282) --- EIPS/eip-5507.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index 6d6c987de77290..e329cde7ff7317 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -30,8 +30,8 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL pragma solidity ^0.8.17; -import "IERC20.sol"; -import "IERC165.sol"; +import "ERC20.sol"; +import "ERC165.sol"; /// @notice Refundable EIP-20 tokens /// @dev The EIP-165 identifier of this interface is `0xf0ca2917` @@ -67,8 +67,8 @@ interface ERC20Refund is ERC20, ERC165 { pragma solidity ^0.8.17; -import "IERC721.sol"; -import "IERC165.sol"; +import "ERC721.sol"; +import "ERC165.sol"; /// @notice Refundable EIP-721 tokens /// @dev The EIP-165 identifier of this interface is `0xe97f3c83` @@ -106,8 +106,8 @@ interface ERC721Refund is ERC721 /* , ERC165 */ { pragma solidity ^0.8.17; -import "IERC1155.sol"; -import "IERC165.sol"; +import "ERC1155.sol"; +import "ERC165.sol"; /// @notice Refundable EIP-1155 tokens /// @dev The EIP-165 identifier of this interface is `0x94029f5c` From f97ecf2ab4808fe83d7d3a812bbdeedab6de226e Mon Sep 17 00:00:00 2001 From: eth-bot <85952233+eth-bot@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:50:30 -0800 Subject: [PATCH 134/274] This PR shouldn't be able to be merged by me --- 404.html | 1 - 1 file changed, 1 deletion(-) diff --git a/404.html b/404.html index c472b4ea0a7810..faf7e23559089f 100644 --- a/404.html +++ b/404.html @@ -18,7 +18,6 @@

404

-

Page not found :(

The requested page could not be found.

From 8e27eca8acf0d926e269b325be4974cdee92660b Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:02:47 -0500 Subject: [PATCH 135/274] Update EIP-5507: Final changes before last call (#6283) * Update EIP-5507: Final changes before last call * Fix walidator issue --- EIPS/eip-5507.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index e329cde7ff7317..6bbb9493300256 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -55,7 +55,7 @@ interface ERC20Refund is ERC20, ERC165 { function refundOf() external view returns (uint256 _wei); /// @notice Gets the first block for which the refund is not active - /// @return block The block beyond which the token cannot be refunded + /// @return block The first block where the token cannot be refunded function refundDeadlineOf() external view returns (uint256 block); } ``` @@ -94,7 +94,7 @@ interface ERC721Refund is ERC721 /* , ERC165 */ { /// @notice Gets the first block for which the refund is not active for a given `tokenId` /// @param tokenId The `tokenId` to query - /// @return block The block beyond which the token cannot be refunded + /// @return block The first block where token cannot be refunded function refundDeadlineOf(uint256 tokenId) external view returns (uint256 block); } ``` @@ -136,7 +136,7 @@ interface ERC1155Refund is ERC1155 /* , ERC165 */ { /// @notice Gets the first block for which the refund is not active for a given `tokenId` /// @param tokenId The `tokenId` to query - /// @return block The block beyond which the token cannot be refunded + /// @return block The first block where the token cannot be refunded function refundDeadlineOf(uint256 tokenId) external view returns (uint256 block); } ``` @@ -145,13 +145,19 @@ interface ERC1155Refund is ERC1155 /* , ERC165 */ { `refundDeadlineOf` uses blocks instead of timestamps, as timestamps are less reliable than block numbers. +The function names of `refund`, `refundOf`, and `refundDeadlineOf` were chosen to fit the naming style of EIP-20, EIP-721, and EIP-1155. + +[EIP-165](./eip-165.md) is required as introspection by DApps would be made significantly harder if it weren't. + +Custom EIP-20 tokens are not supported, as it needlessly increases complexity. + ## Backwards Compatibility No backward compatibility issues were found. ## Security Considerations -Needs discussion. +There is a potential re-entrancy risk with the `refund` function. Make sure to perform the ether transfer **after** the tokens are destroyed (i.e. obey the checks, effects, interactions pattern). ## Copyright From 2dc5c71cba420495ab0085145aaeae5e0c2938df Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:22:14 -0500 Subject: [PATCH 136/274] CI: Make @eth-bot run on every PR action (#6284) --- .github/workflows/auto-review-trigger.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/auto-review-trigger.yml b/.github/workflows/auto-review-trigger.yml index 644e1b01622567..a6f36363a93f55 100644 --- a/.github/workflows/auto-review-trigger.yml +++ b/.github/workflows/auto-review-trigger.yml @@ -1,14 +1,6 @@ on: pull_request_target: - types: - - opened - - reopened - - synchronize - - ready_for_review pull_request_review: - types: - - submitted - - dismissed workflow_dispatch: inputs: pr_number: From b86ce9e53e7cfff49204391f4b004a549ac8ee17 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:29:29 -0500 Subject: [PATCH 137/274] Update EIP-4834: Add changes to increase flexibility (#6285) * Update EIP-4834: Add changes to increase flexibility * Delete IDomain.sol * Delete IDomainAccessControl.sol * Delete IDomainEnumerable.sol * Delete OwnableDomain.sol * Remove reference implementation * Fix typo * Update eip-4834.md --- EIPS/eip-4834.md | 107 ++++++++----------- assets/eip-4834/IDomain.sol | 92 ---------------- assets/eip-4834/IDomainAccessControl.sol | 26 ----- assets/eip-4834/IDomainEnumerable.sol | 17 --- assets/eip-4834/OwnableDomain.sol | 130 ----------------------- 5 files changed, 47 insertions(+), 325 deletions(-) delete mode 100644 assets/eip-4834/IDomain.sol delete mode 100644 assets/eip-4834/IDomainAccessControl.sol delete mode 100644 assets/eip-4834/IDomainEnumerable.sol delete mode 100644 assets/eip-4834/OwnableDomain.sol diff --git a/EIPS/eip-4834.md b/EIPS/eip-4834.md index d497c34b9a54b5..1a17cebfd92581 100644 --- a/EIPS/eip-4834.md +++ b/EIPS/eip-4834.md @@ -8,16 +8,11 @@ status: Review type: Standards Track category: ERC created: 2022-02-22 -requires: 137, 165 --- ## Abstract -This is a standard for generic name resolution with access control. It permits a contract that implements this EIP (referred to as a "domain" hereafter) to be addressable with a more human-friendly name, with a similar purpose to [EIP-137](./eip-137.md) (referred to as "ENS" hereafter). - -Any program that resolves domains should treat domains as equivalent to their resolved addresses. In practice, this means users of DApps that implement this EIP's name resolution may specify an address that looks like `dai.token` instead of `0x6b175474e89094c44da98b954eedeac495271d0f`. In this instance, `dai.token` and `0x6b175474e89094c44da98b954eedeac495271d0f` are not different, unlike ENS, where names are simply keys to be hashed and inputted into a storage contract that then resolves the name to an address. - -Another notable divergence from ENS is that access control can be arbitrarily complex. ENS domains have a defined owner that has full permission to create, update, and delete subdomains, as well as update the metadata of the domain in resolver contracts. While this can be made more strict by delegating control of the ENS domain to a smart contract, this EIP takes a different approach, and permits any access control patterns to be implemented. +This is a standard for generic name resolution with arbitrarily complex access control and resolution. It permits a contract that implements this EIP (referred to as a "domain" hereafter) to be addressable with a more human-friendly name, with a similar purpose to [EIP-137](./eip-137.md) (also known as "ENS"). ## Motivation @@ -34,7 +29,50 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at [IDomain.sol](../assets/eip-4834/IDomain.sol)): ```solidity -interface IDomain is IERC165 { +interface IDomain { + /// @notice Query if a domain has a subdomain with a given name + /// @param name The subdomain to query, in right to left order + /// @return `true` if the domain has a subdomain with the given name, `false` otherwise + function hasDomain(string[] memory name) external view returns (bool); + + /// @notice Fetch the subdomain with a given name + /// @dev This should revert if `hasDomain(name)` is `false` + /// @param name The subdomain to fetch, in right to left order + /// @return The subdomain with the given name + function getDomain(string[] memory name) external view returns (address); +} +``` + +### Name Resolution + +To resolve a name (like `"a.b.c"`), split it by the delimiter (resulting in something like `["a", "b", "c"]`). Set `domain` initially to the root domain, and `path` to be an empty list. + +Pop off the last element of the array (`"c"`) and add it to the path, then call `domain.hasDomain(path)`. If it's `false`, then the domain resolution fails. Otherwise, set the domain to `domain.getDomain(path)`. Repeat until the list of split segments is empty. + +There is no limit to the amount of nesting that is possible. For example, `0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z` would be valid if the root contains `z`, and `z` contains `y`, and so on. + +Here is a solidity function that resolves a name: + +```solidity +function resolve(string[] calldata splitName, IDomain root) public view returns (address) { + IDomain current = root; + string[] memory path = []; + for (uint i = splitName.length - 1; i >= 0; i--) { + // Append to back of list + path.push(splitName[i]); + // Require that the current domain has a domain + require(current.hasDomain(path), "Name resolution failed"); + // Resolve subdomain + current = current.getDomain(path); + } + return current; +} +``` + +### Optional Extension: Registerable + +```solidity +interface IDomainRegisterable is IDomain { //// Events /// @notice Must be emitted when a new subdomain is created (e.g. through `createDomain`) @@ -56,25 +94,13 @@ interface IDomain is IERC165 { /// @param subdomain the old subdomain event SubdomainDelete(address indexed sender, string name, address subdomain); - //// CRUD - - /// @notice Query if a domain has a subdomain with a given name - /// @param name The subdomain to query - /// @return `true` if the domain has a subdomain with the given name, `false` otherwise - function hasDomain(string memory name) external view returns (bool); - - /// @notice Fetch the subdomain with a given name - /// @dev This should revert if `hasDomain(name)` is `false` - /// @param name The subdomain to fetch - /// @return The subdomain with the given name - function getDomain(string memory name) external view returns (address); /// @notice Create a subdomain with a given name /// @dev This should revert if `canCreateDomain(msg.sender, name, pointer)` is `false` or if the domain exists /// @param name The subdomain name to be created /// @param subdomain The subdomain to create - function createDomain(string memory name, address subdomain) external; + function createDomain(string memory name, address subdomain) external payable; /// @notice Update a subdomain with a given name /// @dev This should revert if `canSetDomain(msg.sender, name, pointer)` is `false` of if the domain doesn't exist @@ -117,35 +143,8 @@ interface IDomain is IERC165 { } ``` -As per [EIP-165](./eip-165.md), `supportsInterface(0xe3ffd947)` MUST return `true`. - -### Name Resolution - -To resolve a name (like `"a.b.c"`), split it by the delimiter (resulting in something like `["a", "b", "c"]`). Set `domain` initially to the root domain. - -Pop off the last element of the array (`"c"`), then call `domain.hasDomain(lastElement)`. If it's `false`, then the domain resolution fails. Otherwise, set the domain to `domain.getDomain(lastElement)`. Repeat until the list of split segments is empty. - -There is no limit to the amount of nesting that is possible. For example, `0.1.2.3.4.5.6.7.8.9.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z` would be valid if the root contains `z`, and `z` contains `y`, and so on. - -Here is a solidity function that resolves a name: - -```solidity -function resolve(string[] calldata splitName, IDomain root) public view returns (address) { - IDomain current = root; - for (uint i = splitName.length - 1; i >= 0; i--) { - // Require that the current domain has a domain - require(current.hasDomain(splitName[i]), "Name resolution failed: ); - // Resolve subdomain - current = current.getDomain(splitName[i]); - } - return current; -} -``` - ### Optional Extension: Enumerable -Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at [IDomainEnumerable.sol](../assets/eip-4834/IDomainEnumerable.sol)): - ```solidity interface IDomainEnumerable is IDomain { /// @notice Query all subdomains. Must revert if the number of domains is unknown or infinite. @@ -158,12 +157,8 @@ interface IDomainEnumerable is IDomain { } ``` -As per [EIP-165](./eip-165.md), `supportsInterface(0x8d5fd78e)` MUST return `true` if `IDomainEnumerable` is used. - ### Optional Extension: Access Control -Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at [IDomainAccessControl.sol](../assets/eip-4834/IDomainAccessControl.sol)): - ```solidity interface IDomainAccessControl is IDomain { /// @notice Get if an account can move the subdomain away from the current domain @@ -185,8 +180,6 @@ interface IDomainAccessControl is IDomain { } ``` -As per [EIP-165](./eip-165.md), `supportsInterface(0x1b2e22d2)` MUST return `true` if `IDomainAccessControl` is used. - ## Rationale This EIP's goal, as mentioned in the abstract, is to have a simple interface for resolving names. Here are a few design decisions and why they were made: @@ -205,13 +198,7 @@ This EIP's goal, as mentioned in the abstract, is to have a simple interface for ## Backwards Compatibility -There is no compatibility with ENS because ENS domains are indexed by the hash of the entire domain, while domains under this EIP are indexed by the subdomain name. - -## Reference Implementation - -Some of these implementations use some contracts from OpenZeppelin for ease of understandability. - -Ownable Domain Implementation: [OwnableDomain.sol](../assets/eip-4834/OwnableDomain.sol) +This EIP is general enough to support ENS, but ENS is not general enough to support this EIP. ## Security Considerations diff --git a/assets/eip-4834/IDomain.sol b/assets/eip-4834/IDomain.sol deleted file mode 100644 index e92f842e2bf203..00000000000000 --- a/assets/eip-4834/IDomain.sol +++ /dev/null @@ -1,92 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.9; - -import "@openzeppelin/contracts/interfaces/IERC165.sol"; - -/// @title ERC-4835 Heirarchal Domains Standard -/// @author Pandapip1 -/// @dev https://eips.ethereum.org/EIPS/eip-4835 -interface IDomain is IERC165 { - //// Events - - /// @notice Must be emitted when a new subdomain is created (eg. through `createDomain`.) - /// @param sender msg.sender for createDomain - /// @param name name for createDomain - /// @param subdomain subdomain in createDomain - event SubdomainCreate(address indexed sender, string name, address subdomain); - - /// @notice Must be emitted when the resolved address for a domain is changed (eg. with `setDomain`) - /// @param sender msg.sender for setDomain - /// @param name name for setDomain - /// @param subdomain subdomain in setDomain - /// @param oldSubdomain the old subdomain - event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain); - - /// @notice Must be emitted when a domain is unmapped (eg. with `deleteDomain`) - /// @param sender msg.sender for deleteDomain - /// @param name name for deleteDomain - /// @param subdomain the old subdomain - event SubdomainDelete(address indexed sender, string name, address subdomain); - - - //// CRUD - - /// @notice Query if a domain has a subdomain with a given name - /// @param name The subdomain to query - /// @return `true` if the domain has a subdomain with the given name, `false` otherwise - function hasDomain(string memory name) external view returns (bool); - - /// @notice Fetch the subdomain with a given name - /// @dev This should revert if `hasDomain(name)` is `false` - /// @param name The subdomain to fetch - /// @return The subdomain with the given name - function getDomain(string memory name) external view returns (address); - - /// @notice Create a subdomain with a given name - /// @dev This should revert if `canCreateDomain(msg.sender, name, pointer)` is `false` or if the domain exists - /// @param name The subdomain name to be created - /// @param subdomain The subdomain to create - function createDomain(string memory name, address subdomain) external; - - /// @notice Update a subdomain with a given name - /// @dev This should revert if `canSetDomain(msg.sender, name, pointer)` is `false` of if the domain does not exist - /// @param name The subdomain name to be updated - /// @param subdomain The subdomain to set - function setDomain(string memory name, address subdomain) external; - - /// @notice Delete the subdomain with a given name - /// @dev This should revert if the domain does not exist or if - /// `canDeleteDomain(msg.sender, name, this)` is `false` - /// @param name The subdomain to delete - function deleteDomain(string memory name) external; - - - //// Parent Domain Access Control - - /// @notice Get if an account can create a subdomain with a given name - /// @dev This must return `false` if `hasDomain(name)` is `true`. - /// @param updater The account that may or may not be able to create/update a subdomain - /// @param name The subdomain name that would be created/updated - /// @param subdomain The subdomain that would be set - /// @return Whether an account can update or create the subdomain - function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool); - - /// @notice Get if an account can update or create a subdomain with a given name - /// @dev This must return `false` if `hasDomain(name)` is `false`. - /// If `getDomain(name)` is also a domain, this should return `false` if - /// `getDomain(name).canMoveSubdomain(msg.sender, this, subdomain)` is `false`. - /// @param updater The account that may or may not be able to create/update a subdomain - /// @param name The subdomain name that would be created/updated - /// @param subdomain The subdomain that would be set - /// @return Whether an account can update or create the subdomain - function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool); - - /// @notice Get if an account can delete the subdomain with a given name - /// @dev This must return `false` if `hasDomain(name)` is `false`. - /// If `getDomain(name)` is a domain, this should return `false` if - /// `getDomain(name).canDeleteSubdomain(msg.sender, this, subdomain)` is `false`. - /// @param updater The account that may or may not be able to delete a subdomain - /// @param name The subdomain to delete - /// @return Whether an account can delete the subdomain - function canDeleteDomain(address updater, string memory name) external view returns (bool); -} diff --git a/assets/eip-4834/IDomainAccessControl.sol b/assets/eip-4834/IDomainAccessControl.sol deleted file mode 100644 index 8923a9f138db36..00000000000000 --- a/assets/eip-4834/IDomainAccessControl.sol +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.9; - -import "./IDomain.sol"; - -/// @title ERC-4835 Heirarchal Domains Standard (Access Control Extension) -/// @author Pandapip1 -/// @dev https://eips.ethereum.org/EIPS/eip-4835 -interface IDomainAccessControl is IDomain { - /// @notice Get if an account can move the subdomain away from the current domain - /// @dev May be called by `canSetDomain` of the parent domain - implement access control here!!! - /// @param updater The account that may be moving the subdomain - /// @param name The subdomain name - /// @param parent The parent domain - /// @param newSubdomain The domain that will be set next - /// @return Whether an account can update the subdomain - function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool); - - /// @notice Get if an account can unset this domain as a subdomain - /// @dev May be called by `canDeleteDomain` of the parent domain - implement access control here!!! - /// @param updater The account that may or may not be able to delete a subdomain - /// @param name The subdomain to delete - /// @param parent The parent domain - /// @return Whether an account can delete the subdomain - function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool); -} diff --git a/assets/eip-4834/IDomainEnumerable.sol b/assets/eip-4834/IDomainEnumerable.sol deleted file mode 100644 index ae4161ef27ecbf..00000000000000 --- a/assets/eip-4834/IDomainEnumerable.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.9; - -import "./IDomain.sol"; - -/// @title ERC-4835 Heirarchal Domains Standard (Enumerable Extension) -/// @author Pandapip1 -/// @dev https://eips.ethereum.org/EIPS/eip-4835 -interface IDomainEnumerable is IDomain { - /// @notice Query all subdomains. Must revert if the number of domains is unknown or infinite. - /// @return The subdomain with the given index. - function subdomainByIndex(uint256 index) external view returns (string memory); - - /// @notice Get the total number of subdomains. Must revert if the number of domains is unknown or infinite. - /// @return The total number of subdomains - function totalSubdomains() external view returns (uint256); -} diff --git a/assets/eip-4834/OwnableDomain.sol b/assets/eip-4834/OwnableDomain.sol deleted file mode 100644 index 6af8c8ad62c78c..00000000000000 --- a/assets/eip-4834/OwnableDomain.sol +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.9; - -// NOTE: This is very untested. Do not use! - -import "./IDomain.sol"; -import "./IDomainAccessControl.sol"; -import "./IDomainEnumerable.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165Storage.sol"; -import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; - -contract OwnableDomain is IDomain, IDomainAccessControl, ERC165Storage, Ownable, ERC165Checker { - //// States - mapping(string => address) public subdomains; - mapping(string => bool) public subdomainsPresent; - mapping(string => uint) public subdomainIndeces; - string[] public subdomainList; - - - //// Constructor - - constructor() { - _registerInterface(type(IDomain).interfaceId); - _registerInterface(type(IDomainAccessControl).interfaceId); - } - - - //// CRUD - - function hasDomain(string memory name) public view returns (bool) { - return subdomainsPresent[name]; - } - - function getDomain(string memory name) public view returns (address) { - require(this.hasDomain(name)); - return subdomains[name]; - } - - function createDomain(string memory name, IDomain subdomain) public { - require(!this.hasDomain(name)); - require(this.canCreateDomain(msg.sender, name, subdomain)); - - subdomainsPresent[name] = true; - subdomains[name] = subdomain; - - subdomainIndeces[name] = subdomainList.length; - subdomainList.push(name); - - emit SubdomainCreate(msg.sender, name, subdomain); - } - - function setDomain(string memory name, address subdomain) public { - require(this.hasDomain(name)); - require(this.canSetDomain(msg.sender, name, subdomain)); - - address oldSubdomain = subdomains[name]; - subdomains[name] = subdomain; - - emit SubdomainUpdate(msg.sender, name, subdomain, oldSubdomain); - } - - function deleteDomain(string memory name) public { - require(this.hasDomain(name)); - require(this.canDeleteDomain(msg.sender, name)); - - subdomainsPresent[name] = false; // Only need to mark it as deleted - delete subdomainList[subdomainIndeces[name]]; // Remove subdomain from list - - emit SubdomainDelete(msg.sender, name, subdomains[name]); - } - - - //// Parent Domain Access Control - - function canCreateDomain(address updater, string memory name, address subdomain) public view returns (bool) { - // Existence Check - if (this.hasDomain(name)) { - return false; - } - - // Is user owner - bool isTheOwner = this.owner() == updater; - - // Return - return isTheOwner; - } - - function canSetDomain(address updater, string memory name, address subdomain) public view returns (bool) { - // Existence Check - if (!this.hasDomain(name)) { - return false; - } - - // Is user owner - bool isTheOwner = this.owner() == updater; - - // Auth Check - bool isMovable = this.supportsInterface(this.getDomain(name), type(IDomainAccessControl).interfaceId) && IDomainAccessControl(this.getDomain(name)).canMoveSubdomain(updater, name, this, subdomain); - - // Return - return (isTheOwner || isMovable); - } - - function canDeleteDomain(address updater, string memory name) public view returns (bool) { - // Existence Check - if (!this.hasDomain(name)) { - return false; - } - - // Is user owner - bool isTheOwner = this.owner() == updater; - - // Auth Check - bool isDeletable = this.supportsInterface(this.getDomain(name), type(IDomainAccessControl).interfaceId) && IDomainAccessControl(this.getDomain(name)).canDeleteDomain(updater, name, this); - - // Return - return isTheOwner || isDeletable; - } - - //// Subdomain Access Control - - function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) public virtual view returns (bool) { - return this.owner() == updater; - } - - function canDeleteSubdomain(address updater, string memory name, IDomain parent) public virtual view returns (bool) { - return this.owner() == updater; - } -} From fb7ea45bf315a46906d2936bd5f6498cf0db6c7f Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 16:37:20 -0500 Subject: [PATCH 138/274] Update EIP-4834: Remove old links (#6288) --- EIPS/eip-4834.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/EIPS/eip-4834.md b/EIPS/eip-4834.md index 1a17cebfd92581..dbd29f8d4c8e29 100644 --- a/EIPS/eip-4834.md +++ b/EIPS/eip-4834.md @@ -26,8 +26,6 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S ### Contract Interface -Solidity Interface with NatSpec & OpenZeppelin v4 Interfaces (also available at [IDomain.sol](../assets/eip-4834/IDomain.sol)): - ```solidity interface IDomain { /// @notice Query if a domain has a subdomain with a given name @@ -222,10 +220,6 @@ Clients should help by warning if `canMoveSubdomain` or `canDeleteSubdomain` for Parent domains have full control of name resolution for their subdomains. If a particular domain is linked to `a.b.c`, then `b.c` can, depending on its code, set `a.b.c` to any domain, and `c` can set `b.c` itself to any domain. -#### Examples: Parent Domain Resolution - -The reference Ownable domain implementation: [OwnableDomain.sol](../assets/eip-4834/OwnableDomain.sol) - #### Mitigation: Parent Domain Resolution Before acquiring a domain that has been pre-linked, it is recommended to always have the contract **and** all the parents up to the root audited. From 6cab84b017b57956eef8bb67626323b22a57d8dd Mon Sep 17 00:00:00 2001 From: Dror Tirosh Date: Sat, 7 Jan 2023 02:21:32 +0200 Subject: [PATCH 139/274] Update eip-2771.md (#6290) --- EIPS/eip-2771.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index 1c008c157547bd..b2d0542e1d97b2 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -75,7 +75,7 @@ function isTrustedForwarder(address forwarder) external view returns(bool); 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. +`isTrustedForwarder` function MAY be called on-chain, and as such gas restrictions MUST be put in place. It SHOULD NOT consume more than 50,000 gas ## Rationale From 932574d7581de03764eb00b9e2c34b2d7c5ce265 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Fri, 6 Jan 2023 20:26:21 -0500 Subject: [PATCH 140/274] Update EIP-5568: Micah's review (#6291) * Update EIP-5568: Micah's review * Remove Well-known from title --- EIPS/eip-5568.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-5568.md b/EIPS/eip-5568.md index 6b18a057643051..495bf6dc8986e2 100644 --- a/EIPS/eip-5568.md +++ b/EIPS/eip-5568.md @@ -1,12 +1,12 @@ --- eip: 5568 -title: Required Action Signals Using Revert Reasons +title: Revert Reason for Required Actions description: Signal to wallets that an action is needed by returning a custom revert code author: Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/eip-5568-revert-signals/10622 status: Review type: Standards Track -category: Interface +category: ERC created: 2022-08-31 requires: 140 --- @@ -27,27 +27,25 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL ### Custom Revert Reason -To send a signal to a wallet, a compliant smart contract MUST revert with the following error: +To signal an action needs to be taken, a compliant smart contract MUST revert with the following error: ```solidity -error WalletSignal24(uint8 number_hint, uint24 instruction_id, bytes instruction_data) +error WalletSignal24(uint24 instruction_id, bytes instruction_data) ``` -The `number_hint` is an estimate of the number of signals that will be sent after the current signal. If a guess is availabe, `number_hint` MUST be that estimate. If a guess is unavailable, `number_hint` MUST be `0`. - The `instruction_id` of an instruction defined by an EIP MUST be its EIP number unless there are exceptional circumstances (be reasonable). An EIP MUST define exactly zero or one `instruction_id`. The structure of the instruction data for any `instruction_id` MUST be defined by the EIP that defines the `instruction_id`. -### Signal Response +### Responding to a Revert -Before submitting a transaction to the mempool, it MUST be evaluated locally. If it reverts and the revert signature matches the custom error, then it MUST be treated as a signal. (It is RECOMMENDED for wallets to show a warning if the transaction reverts, even if the revert is not a signal). +Before submitting a transaction to the mempool, it MUST be evaluated locally. If it reverts and the revert signature matches the custom error, then the following applies. -The `number_hint`, `instruction_id`, and `instruction_data` MUST be parsed from the revert data. It is RECOMMENDED for wallets to show a progress indicator using the `number_hint`. The instruction SHOULD be evaluated as per the relevant EIP. If the instruction is not supported by the wallet, it MUST display an error to the user indicating that is the case. The wallet MUST then re-evaluate the transaction, except if an instruction explicitly states that the transaction MUST NOT be re-evaluated. +The `instruction_id`, and `instruction_data` MUST be parsed from the revert data. The instruction SHOULD be evaluated as per the relevant EIP. If the instruction is not supported by the wallet, it MUST display an error to the user indicating that is the case. The wallet MUST then re-evaluate the transaction, except if an instruction explicitly states that the transaction MUST NOT be re-evaluated. -If an instruction is invalid, or the `number_hint`, `instruction_id`, and `instruction_data` cannot be parsed, then an error MUST be displayed to the user indicating that is the case. +If an instruction is invalid, or the `instruction_id`, and `instruction_data` cannot be parsed, then an error MUST be displayed to the user indicating that is the case. The transaction MUST NOT be re-evaluated. ## Rationale -This EIP was explicitly optimized for deployment gas cost and simplicity. It is expected that libraries will eventually be developed that makes sending and receiving signals more developer-friendly. +This EIP was explicitly optimized for deployment gas cost and simplicity. It is expected that libraries will eventually be developed that makes sending and receiving these well-known reverts more developer-friendly. ## Backwards Compatibility From f8a3cc25f903713a986c4614348b6fe56fb46f2f Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sat, 7 Jan 2023 15:58:15 -0500 Subject: [PATCH 141/274] Update EIP-5920: Prepare for Review (#6289) --- EIPS/eip-5920.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index 13d7d6f0ee0f99..569071de44e136 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -12,7 +12,7 @@ created: 2022-03-14 ## Abstract -This EIP introduces a new opcode, `PAY(addr, val)`, that transfers `val` wei to the address `addr` without calling it. After `FORK_BLKNUM`, this opcode is added to the EVM. +This EIP introduces a new opcode, `PAY(addr, val)`, that transfers `val` wei to the address `addr` without calling it. ## Motivation @@ -22,8 +22,7 @@ Currently, to send ether to an address requires you to call a function of that a | Parameter | Value | | ------------------- | ------- | -| `FORK_BLKNUM` | TBD | -| `PAY_OPCODE` | TBD | +| `PAY_OPCODE` | `0xf9` | | `BASE_GAS_COST` | `8600` | | `COLD_GAS_COST` | `11100` | | `CREATION_GAS_COST` | `32600` | @@ -47,7 +46,7 @@ The order of arguments mimicks that of `CALL`, which pops `addr` before `val`. B ## Backwards Compatibility -Needs discussion. +This change requires a hard fork. ## Security Considerations From e7dd3fc058a9359895c5d5d7a6ddaf3da1e688e3 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sat, 7 Jan 2023 18:14:01 -0500 Subject: [PATCH 142/274] Update EIP-5920: Update abstract (#6292) --- EIPS/eip-5920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index 569071de44e136..4ce6395152349f 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -12,7 +12,7 @@ created: 2022-03-14 ## Abstract -This EIP introduces a new opcode, `PAY(addr, val)`, that transfers `val` wei to the address `addr` without calling it. +This EIP introduces a new opcode, `PAY`, taking two stack parameters, `addr` and `val`, that transfers `val` wei to the address `addr` without calling any of its functions. ## Motivation From 14c4fbd6f625c9cfc398fa1e3d86f19f0b135e3e Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sat, 7 Jan 2023 19:42:29 -0500 Subject: [PATCH 143/274] Update EIP-6188: Fix minor typo (#6296) --- EIPS/eip-6188.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6188.md b/EIPS/eip-6188.md index 3b6cb1da12716b..2166cd8b18f11b 100644 --- a/EIPS/eip-6188.md +++ b/EIPS/eip-6188.md @@ -41,7 +41,7 @@ This EIP requires a protocol upgrade, since it modifies consensus rules. The fur ## Security Considerations -As it is infeasible for contract accounts to get to the nonce limit, any potential problems with opcodes that depend on the value of an account's nonce can be safely ignored. +As it is not feasible for contract accounts to get to the nonce limit, any potential problems with opcodes that depend on the value of an account's nonce can be safely ignored. ## Copyright From d256c38fb6b806c2886e0a5f1e4186a80814a334 Mon Sep 17 00:00:00 2001 From: Francisco Date: Sun, 8 Jan 2023 00:05:30 -0300 Subject: [PATCH 144/274] EIP-5267: Clarify use of extensions and add event (#6297) * Add clarity on meaning of extensions * Add event on change --- EIPS/eip-5267.md | 52 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5267.md b/EIPS/eip-5267.md index 7cf07d57935764..e26892def69231 100644 --- a/EIPS/eip-5267.md +++ b/EIPS/eip-5267.md @@ -41,9 +41,13 @@ The return values of this function MUST describe the domain separator that is us - `fields`: A bit map where bit `i` is set to 1 if and only if domain field `i` is present (`0 ≤ i ≤ 4`). Bits are read from least significant to most significant, and fields are indexed in the order that is specified by EIP-712, identical to the order in which they are listed in the function type. - `name`, `version`, `chainId`, `verifyingContract`, `salt`: The value of the corresponding field in `EIP712Domain`, if present according to `fields`. If the field is not present, the value is unspecified. The semantics of each field is defined in EIP-712. -- `extensions`: A list of EIP numbers that specify additional fields in the domain. The method to obtain the value for each of these additional fields and any conditions for inclusion are expected to be specified in the respective EIP. The value of `fields` does not affect their inclusion. +- `extensions`: A list of EIP numbers, each of which MUST refer to an EIP that extends EIP-712 with new domain fields, along with a method to obtain the value for those fields, and potentially conditions for inclusion. The value of `fields` does not affect their inclusion. -The return values of this function (equivalently, its EIP-712 domain) MAY change throughout the lifetime of a contract, but changes SHOULD NOT be frequent. The `chainId` field, if used, SHOULD change to mirror the [EIP-155](./eip-155.md) id of the underlying chain. +The return values of this function (equivalently, its EIP-712 domain) MAY change throughout the lifetime of a contract, but changes SHOULD NOT be frequent. The `chainId` field, if used, SHOULD change to mirror the [EIP-155](./eip-155.md) id of the underlying chain. Contracts MAY emit the event `EIP712DomainChanged` defined below to signal that the domain could have changed. + +```solidity +event EIP712DomainChanged(); +``` ## Rationale @@ -106,14 +110,22 @@ Assuming this contract is on Ethereum mainnet and its address is 0x0000000000000 A domain object can be constructed based on the return values of an `eip712Domain()` invocation. ```javascript -const fieldNames = ['name', 'version', 'chainId', 'verifyingContract', 'salt']; +/** Retrieves the EIP-712 domain of a contract using EIP-5267 without extensions. */ +async function getDomain(contract) { + const { fields, name, version, chainId, verifyingContract, salt, extensions } = + await contract.eip712Domain(); -/** Builds a domain object based on the values obtained by calling `eip712Domain()` in a contract. */ -function buildDomain(fields, name, version, chainId, verifyingContract, salt, extensions) { if (extensions.length > 0) { - throw Error("extensions not implemented"); + throw Error("Extensions not implemented"); } + return buildBasicDomain(fields, name, version, chainId, verifyingContract, salt); +} + +const fieldNames = ['name', 'version', 'chainId', 'verifyingContract', 'salt']; + +/** Builds a domain object without extensions based on the return values of `eip712Domain()`. */ +function buildBasicDomain(fields, name, version, chainId, verifyingContract, salt) { const domain = { name, version, chainId, verifyingContract, salt }; for (const [i, field] of fieldNames.entries()) { @@ -126,6 +138,34 @@ function buildDomain(fields, name, version, chainId, verifyingContract, salt, ex } ``` +#### Extensions + +Suppose EIP-XYZ defines a new field `subdomain` of type `bytes32` and a function `getSubdomain()` to retrieve its value. + +The function `getDomain` from above would be extended as follows. + +```javascript +/** Retrieves the EIP-712 domain of a contract using EIP-5267 with support for EIP-XYZ. */ +async function getDomain(contract) { + const { fields, name, version, chainId, verifyingContract, salt, extensions } = + await contract.eip712Domain(); + + const domain = buildBasicDomain(fields, name, version, chainId, verifyingContract, salt); + + for (const n of extensions) { + if (n === XYZ) { + domain.subdomain = await contract.getSubdomain(); + } else { + throw Error(`EIP-${n} extension not implemented`); + } + } + + return domain; +} +``` + +Additionally, the type of the `EIP712Domain` struct needs to be extended with the `subdomain` field. This is left out of scope of this reference implementation. + ## Security Considerations While this EIP allows a contract to specify a `verifyingContract` other than itself, as well as a `chainId` other than that of the current chain, user-agents and applications should in general validate that these do match the contract and chain before requesting any user signatures for the domain. This may not always be a valid assumption. From 8061f8e2243eaae829d1fa91f7a763c889aca371 Mon Sep 17 00:00:00 2001 From: Firn Protocol <93839494+firnprotocol@users.noreply.github.com> Date: Sun, 8 Jan 2023 12:35:02 -0500 Subject: [PATCH 145/274] more minor changes to 5630 (#6298) --- EIPS/eip-5630.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5630.md b/EIPS/eip-5630.md index db1ba3b0eed985..45cea4e9d3a619 100644 --- a/EIPS/eip-5630.md +++ b/EIPS/eip-5630.md @@ -1,7 +1,7 @@ --- eip: 5630 title: New approach for encryption / decryption -description: defines a specification for encryption and decryption using deterministically derived, pseudorandom keys. +description: defines a specification for encryption and decryption using Ethereum wallets. author: Firn Protocol (@firnprotocol), Fried L. Trout, Weiji Guo (@weijiguo) discussions-to: https://ethereum-magicians.org/t/eip-5630-encryption-and-decryption/10761 status: Draft @@ -97,7 +97,7 @@ contract ERC5630 { function encryptTo(bytes memory plaintext, bytes32 randomness) public view - returns (string memory version, bytes memory ciphertext); + returns (bytes memory ciphertext); } ``` From 7debfefdfa7f02bee40f54d033777544b1ecf601 Mon Sep 17 00:00:00 2001 From: Sean Date: Mon, 9 Jan 2023 17:08:51 +1100 Subject: [PATCH 146/274] Make signature of signature optional (#6302) Although forcing signatures upon the vendors would be useful, allowing third parties and users to create their own receipts that are valid (but ultimately not legal receipts) does offer some opportunites for projects. --- EIPS/eip-5570.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5570.md b/EIPS/eip-5570.md index 1b33d2966c58e0..ab27f62db8158c 100644 --- a/EIPS/eip-5570.md +++ b/EIPS/eip-5570.md @@ -46,7 +46,7 @@ The JSON schema is composed of 2 parts. The root schema contains high level deta "id": "receipt.json#", "description": "Receipt Schema for Digital Receipt Non-Fungible Tokens", "type": "object", - "required": ["name", "description", "image", "receipt", "signature"], + "required": ["name", "description", "image", "receipt"], "properties": { "name": { "title": "Name", @@ -257,7 +257,7 @@ The JSON schema is composed of 2 parts. The root schema contains high level deta ## Rationale -The schema introduced complies with EIP-721's metadata extension, conveniently allowing previous tools for viewing NFTs to show our receipts. The new property "receipt" contains our newly provided receipt structure and the signature property requires the vendor to digitally sign the receipt structure. +The schema introduced complies with EIP-721's metadata extension, conveniently allowing previous tools for viewing NFTs to show our receipts. The new property "receipt" contains our newly provided receipt structure and the signature property optionally allows the vendor to digitally sign the receipt structure. ## Backwards Compatibility From b0ace5758c494b5c7d7a481de81d2113da6ecdf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 9 Jan 2023 14:42:48 +0100 Subject: [PATCH 147/274] EIP-3860: Change the failure to OOG (#6249) * EIP-3860: Change the failure to OOG Change the failure in case of exceeding the initcode size limit for CREATE instructions to be OOG (same as the other check and many others). * EIP-3860: Fix typo Co-authored-by: Alex Stokes Co-authored-by: Alex Stokes --- EIPS/eip-3860.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-3860.md b/EIPS/eip-3860.md index a6a0a03374e7ff..51fb6003054b88 100644 --- a/EIPS/eip-3860.md +++ b/EIPS/eip-3860.md @@ -56,8 +56,8 @@ We define `initcode_cost(initcode)` to equal `INITCODE_WORD_COST * ceil(len(init 1. If length of transaction data (`initcode`) in a create transaction exceeds `MAX_INITCODE_SIZE`, transaction is invalid. (*Note that this is similar to transactions considered invalid for not meeting the intrinsic gas cost requirement.*) 2. For a create transaction, extend the transaction data cost formula to include `initcode_cost(initcode)`. (*Note that this is included in transaction intrinsic cost, i.e. transaction with not enough gas to cover initcode cost is invalid.*) -3. If length of `initcode` to `CREATE` or `CREATE2` instructions exceeds `MAX_INITCODE_SIZE`, instruction execution ends with the result `0` pushed on the stack. Gas for initcode execution is not deducted and caller's nonce is not incremented in this case. -4. For the `CREATE` and `CREATE2` instructions charge an extra gas cost equaling to `initcode_cost(initcode)`. This cost is deducted before the calculation of the resulting contract address and the execution of `initcode`. (*Note that this means before or at the same time as the hashing cost is applied in `CREATE2`.*) If the instruction fails due to the `initcode` length check of point 3, this cost is not deducted. +3. If length of `initcode` to `CREATE` or `CREATE2` instructions exceeds `MAX_INITCODE_SIZE`, instruction execution exceptionally aborts (as if it runs out of gas). +4. For the `CREATE` and `CREATE2` instructions charge an extra gas cost equaling to `initcode_cost(initcode)`. This cost is deducted before the calculation of the resulting contract address and the execution of `initcode`. (*Note that this means before or at the same time as the hashing cost is applied in `CREATE2`.*) ## Rationale @@ -94,7 +94,7 @@ The initcode cost for create transaction data (0.0625 gas per byte) is negligibl ### How to report initcode limit violation? -We specified that initcode size limit violation for `CREATE`/`CREATE2` results in `0` on stack. Most checks in these instructions are specified this way, except for 3 checks not specific to creation instructions (stack underflow, out of gas, static call violation). In these three cases the entire execution is exceptionally aborted. However, we decided to be consistent with the majority of the possible error conditions in order to keep the specification and implementations simple. +We specified that initcode size limit violation for `CREATE`/`CREATE2` results in exceptional abort of the execution. This places it in the group of early out-of-gas checks, including: stack underflow, memory expansion, static call violation, initcode hashing cost, and initcode cost introduced by this EIP. They precede the later "light" checks: call depth and balance. The choice gives consistency to the order of checks and lowers implementation complexity (out-of-gas checks can be performed in any order). ## Backwards Compatibility From 6a54217df7e3a2855f7117ec7285259e96038446 Mon Sep 17 00:00:00 2001 From: chaals Date: Mon, 9 Jan 2023 18:13:10 +0100 Subject: [PATCH 148/274] update EIP3220 to give 8 bytes for Native Chain ID (#6272) --- EIPS/eip-3220.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-3220.md b/EIPS/eip-3220.md index 991396963f2ce8..949413a3f24231 100644 --- a/EIPS/eip-3220.md +++ b/EIPS/eip-3220.md @@ -45,10 +45,10 @@ Hence there is need for a more robust blockchain identifier that will overcome t | Name | Size(bytes) | Description | |---------------|-------------|-------------| | Truncated Block Hash | 16 | This is the block hash of the genesis block or the block hash of of the block immediate prior to the fork for a fork of a blockchain. The 16 bytes is the 16 least significant bytes, assuming network byte order.| -|Native Chain ID| 4 | This is the **Chain Id** value that should be used with the blockchain when signing transactions. For blockchains that do not have a concept of **Chain Id**, this value is zero.| +|Native Chain ID| 8 | This is the **Chain Id** value that should be used with the blockchain when signing transactions. For blockchains that do not have a concept of **Chain Id**, this value is zero.| |Chain Type| 2 | Reserve 0x00 as undefined chaintype. 0x01 as mainnet type. 0x1[0-A]: testnet, 0x2[0-A]: private development network| | Governance Identifier | 2 | For new blockchains, a governance_identifier can be specified to identify an original **owner** of a blockchain, to help settle forked / main chain disputes. For all existing blockchains and for blockchains that do not have the concept of an **owner**, this field is zero. | -| Reserved | 7 | Reserved for future use. Use 000000 for now. | +| Reserved | 3 | Reserved for future use. Use 000000 for now. | | Checksum | 1 | Used to verify the integrity of the identifier. This integrity check is targeted at detecting Crosschain Identifiers mis-typed by human users. The value is calculated as the truncated SHA256 message digest of the rest of the identifier, using the least significant byte, assuming network byte order. Note that this checksum byte only detects integrity with a probability of one in 256. This probability is adequate for the intended usage of detecting typographical errors by people manually entering the Crosschain Identifier. | @@ -58,7 +58,8 @@ We have considered various alternative specifications such as using a random uni ## Backwards Compatibility -Crosschainid can be backward compatible with EIP-155. The crosschain id contains a 4 byte segment to record the chainid based on EIP-155. +Crosschainid can be backward compatible with EIP-155. The crosschain id contains an 8 byte segment to record the `Native Chain ID`. +For Ethereum chains, that can be used for a value intended to be used with EIP-155. ## Security Considerations From 01cfec7f09a116a86e77d4f90296e4433ba21d08 Mon Sep 17 00:00:00 2001 From: Sean Date: Tue, 10 Jan 2023 04:13:56 +1100 Subject: [PATCH 149/274] Update EIP-5570: Move to review (#6303) * move ERC5570 to review * linter over 5570 --- EIPS/eip-5570.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-5570.md b/EIPS/eip-5570.md index ab27f62db8158c..04ed1d66f1a462 100644 --- a/EIPS/eip-5570.md +++ b/EIPS/eip-5570.md @@ -4,7 +4,7 @@ title: Digital Receipt Non-Fungible Tokens description: Non-Fungible Tokens as digital receipts for physical purchases, where the metadata represents a JSON receipt author: Sean Darcy (@darcys22) discussions-to: https://ethereum-magicians.org/t/idea-standard-digital-receipts-using-erc-721/9908 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-09-01 @@ -33,14 +33,17 @@ One of the major roadblocks to fully automating our finance world has been the d ## Specification Transaction Flow: + - A customer purchases an item from an online retailer, checking out leads the customer to an option to mint a NFT. - The smart contract provides the user with a Digital Receipt Non-Fungible Token. - When fulfilling the order, the retailer will upload the digital receipt specified in in the JSON schema below as the metadata to the previously minted NFT. ### Digital Receipt JSON Schema + The JSON schema is composed of 2 parts. The root schema contains high level details of the receipt (for example Date and Vendor) and another schema for the optionally recurring line items contained in the receipt. #### Root Schema + ```json { "id": "receipt.json#", @@ -182,6 +185,7 @@ The JSON schema is composed of 2 parts. The root schema contains high level deta ``` #### Line Items Schema + ```json { "type": "object", @@ -267,8 +271,6 @@ This standard is an extension of EIP-721. It is compatible with both optional ex The data stored in the receipt contains personally identifying information. This information should be encrypted to ensure privacy for the customer. - - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). From 43ee7f9e64fde082f2712070d7b62a37650cb4b0 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Mon, 9 Jan 2023 09:21:05 -0800 Subject: [PATCH 150/274] clarify signed blob transaction hashing (#6238) * clarify how to compute the tx-hash of a signed blob transaction for non-signing purposes * Update EIPS/eip-4844.md Co-authored-by: Micah Zoltu * Update EIPS/eip-4844.md Co-authored-by: Micah Zoltu Co-authored-by: Micah Zoltu --- EIPS/eip-4844.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index df8423c5b84df3..15e66ea3e4f404 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -174,6 +174,13 @@ def get_origin(tx: SignedBlobTransaction) -> Address: return ecrecover(tx_hash(tx), int(sig.y_parity)+27, sig.r, sig.s) ``` +The transaction hash of a signed blob transaction should be computed as: + +```python +def signed_tx_hash(tx: SignedBlobTransaction) -> Bytes32: + return keccak256(BLOB_TX_TYPE + ssz.serialize(tx)) +``` + ### Header extension The current header encoding is extended with a new 256-bit unsigned integer field `excess_data_gas`. This is the running From 7cd2762b54b98c6298b8c952fcfadeabf8b146d6 Mon Sep 17 00:00:00 2001 From: Roberto Bayardo Date: Mon, 9 Jan 2023 12:46:50 -0800 Subject: [PATCH 151/274] change blob tx_hash to use serialize instead of hash_tree_root to make it more consistent with computing the regular blob tx hash (#6241) --- EIPS/eip-4844.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index 15e66ea3e4f404..b380662ef7088a 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -164,17 +164,17 @@ The execution layer verifies the wrapper validity against the inner `Transaction The signature is verified and `tx.origin` is calculated as follows: ```python -def tx_hash(tx: SignedBlobTransaction) -> Bytes32: +def unsigned_tx_hash(tx: SignedBlobTransaction) -> Bytes32: # The pre-image is prefixed with the transaction-type to avoid hash collisions with other tx hashers and types - return keccak256(BLOB_TX_TYPE + ssz.hash_tree_root(tx.message)) + return keccak256(BLOB_TX_TYPE + ssz.serialize(tx.message)) def get_origin(tx: SignedBlobTransaction) -> Address: sig = tx.signature # v = int(y_parity) + 27, same as EIP-1559 - return ecrecover(tx_hash(tx), int(sig.y_parity)+27, sig.r, sig.s) + return ecrecover(unsigned_tx_hash(tx), int(sig.y_parity)+27, sig.r, sig.s) ``` -The transaction hash of a signed blob transaction should be computed as: +The hash of a signed blob transaction should be computed as: ```python def signed_tx_hash(tx: SignedBlobTransaction) -> Bytes32: From 22061ae55a4a08969ef23474b0c17d1873bbe9e6 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:05:02 -0500 Subject: [PATCH 152/274] Update EIP-4834: Move to Last Call (#6287) --- EIPS/eip-4834.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-4834.md b/EIPS/eip-4834.md index dbd29f8d4c8e29..9310db9baf2c0e 100644 --- a/EIPS/eip-4834.md +++ b/EIPS/eip-4834.md @@ -4,7 +4,8 @@ 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: Review +status: Last Call +last-call-deadline: 2023-01-20 type: Standards Track category: ERC created: 2022-02-22 From d6f47fc5ccd769ad9de587dfeee5275e9187b46a Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:15:32 -0500 Subject: [PATCH 153/274] Update EIP-5507: Move to review (#6186) --- EIPS/eip-5507.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5507.md b/EIPS/eip-5507.md index 6bbb9493300256..683cc10c034325 100644 --- a/EIPS/eip-5507.md +++ b/EIPS/eip-5507.md @@ -4,7 +4,7 @@ title: Refundable Tokens description: Adds refund functionality to EIP-20, EIP-721, and EIP-1155 tokens author: elie222 (@elie222), Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/eip-5507-refundable-nfts/10451 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-08-19 From 914695888ca2ecd395bff92987b18312d61f62d5 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:16:08 -0500 Subject: [PATCH 154/274] Update EIP-5920: Move to Review (#6295) --- EIPS/eip-5920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index 4ce6395152349f..99922143cdfae2 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -4,7 +4,7 @@ title: PAY opcode description: Introduces a new opcode, PAY, to send ether to an address without calling any of its functions author: Pandapip1 (@Pandapip1), Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/eip-5920-pay-opcode/11717 -status: Draft +status: Review type: Standards Track category: Core created: 2022-03-14 From a041be396ad6c503c9c89ff18284d156d54c5db0 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 10 Jan 2023 10:20:08 -0500 Subject: [PATCH 155/274] Add EIP-6189: Alias Contracts (#6189) * Add contract aliases * Add CREATE behavior * Assign EIP-6189 * Rename eip-contract-alias.md to eip-6189.md * Add discussions URL * Add RFC 8174 * Recognize EOA Transactions * Add contract-linking to CREATE and CREATE2 * Expand Motivation * Add RPC endpoint change for eth_getStorageAt * Fix a few isssues with the EIP * Fix another small issue * Clear check annotations --- EIPS/eip-6189.md | 98 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 EIPS/eip-6189.md diff --git a/EIPS/eip-6189.md b/EIPS/eip-6189.md new file mode 100644 index 00000000000000..2dc6ccc08550f9 --- /dev/null +++ b/EIPS/eip-6189.md @@ -0,0 +1,98 @@ +--- +eip: 6189 +title: Alias Contracts +description: Allows the creation of contracts that forward calls to other contracts +author: Pandapip1 (@Pandapip1) +discussions-to: https://ethereum-magicians.org/t/eip-6190-functional-selfdestruct/12232 +status: Draft +type: Standards Track +category: Core +created: 2022-12-20 +requires: 2929, 6188 +--- + +## Abstract + +This EIP allows contracts to be turned into "alias contracts" using a magic nonce. Alias contracts automatically forward calls to other contracts. + +## Motivation + +This EIP is not terribly useful on its own, as it adds additional computation and gas costs without any useful side effects. However, in conjunction with another EIP, it can be used to make SELFDESTRUCT compatible with Verkle trees. + +## 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 and RFC 8174. + +### Definitions + +A contract is an alias contract if its nonce is `2^64-1`, and its contract code is equal to `0x1`. + +### Prerequisites + +[EIP-6188](./eip-6188.md) MUST be used to protect the magic nonce value of `2^64-1`. + +### Opcode Changes + +#### `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`, and EOA Transactions + +The "callee" refers to the account that is being called. + +If the nonce of the callee is `2^64-1`, the call is forwarded to the address stored in the `0`th storage slot of the callee (as if the callee was the address stored in the `0`th storage slot of the callee). This repeats until a non-alias contract is reached. The `CALLER` remains unchanged. + +If there is more than one alias contract in the chain, the original callee and all subsequent callees (except the last one) have their `0`th storage slot set to the address of the final non-alias contract. Then, the call is forwarded as usual. **This occurs even in a read-only context.** + +For example, if `A` is an alias contract that forwards calls to `B`, which is an alias contract that forwards calls to `C`, then `A`'s `0`th storage slot is set to `C`'s address. Then, the call is forwarded to `C`. + +The `CALL`, `CALLCODE`, `DELEGATECALL`, and `STATICCALL` opcodes and EOA Transactions MUST cost an `25` gas per account accessed in this manner (including the final one, and including if no aliased accounts were used), in addition to all the regular costs incurred by accessing accounts (see [EIP-2929](./eip-2929.md)). For every account whose `0`th storage slot is updated, those opcodes must also cost an additional `5000` gas. + +If an infinite loop occurs, the transaction runs out of gas and reverts. + +#### `EXTCODEHASH`, `EXTCODECOPY`, `EXTCODESIZE`, and `BALANCE` + +The "accessed account" refers to the account that is being accessed (i.e. the account whose code is being accessed, or the account whose balance is being accessed). + +Similar to the `CALL` family of opcodes, if the nonce of the accessed account is `2^64-1`, the accessed account is replaced with the address stored in the `0`th storage slot of the accessed account. This repeats until a non-alias contract is reached. + +If there is more than one alias contract in the chain, the original accessed account and all subsequent accessed accounts (except the last one) have their `0`th storage slot set to the address of the final non-alias contract. Then, the accessed account is replaced as usual. + +The `EXTCODEHASH`, `EXTCODECOPY`, `EXTCODESIZE`, and `BALANCE` opcodes MUST cost an `25` gas per account accessed in this manner (including the final one, and including if no aliased accounts were used), in addition to all the regular costs incurred by accessing accounts (see [EIP-2929](./eip-2929.md)). For every account whose `0`th storage slot is updated, those opcodes must also cost an additional `5000` gas. + +If an infinite loop occurs, the transaction runs out of gas and reverts. + +#### `CREATE` and `CREATE2` + +If `CREATE` or `CREATE2` would fail because there is already a an account at the address, and that contract's code is `0x1`, and its nonce is `2^64-1`, then instead of failing, an attempt should be made to create a contract at the address stored in the `0`th storage slot of the existing contract. This repeats until a non-alias contract is reached, at which point either the creation succeeds, or it fails because there is already an account at the address. + +Regardless of if creation succeeds, if there is more than one alias contract in the chain, the original accessed account and all subsequent accessed accounts (except the last one) have their `0`th storage slot set to the address of the final non-alias contract. Then, the accessed account is replaced as usual. + +The `CREATE` and `CREATE2` opcodes MUST cost an `25` gas per account accessed in this manner (including the final one, and including if no aliased accounts were used), in addition to all the regular costs incurred by accessing accounts (see [EIP-2929](./eip-2929.md)). For every account whose `0`th storage slot is updated, those opcodes must also cost an additional `5000` gas. + +If an infinite loop occurs, the transaction runs out of gas and reverts. + +### RPC Endpoint Changes + +#### `eth_getStorageAt` + +The `eth_getStorageAt` RPC endpoint must error if the target contract has a contract code of `0x1` and a nonce of `2^64-1`. + +## Rationale + +The additional gas cost of `25` represents the cost of fetching the nonce and comparing it to the given value. + +`eth_getStorageAt` was modified to throw an error because of alias contracts' special behavior. + +The nonce of `2^64-1` was chosen since it is the nonce protected by [EIP-6188](./eip-6188.md). + +The contract code of `0x1` was chosen arbitrarily. A nonzero code was chosen just in case a non-alias contract with nonce `2^64-1` somehow had its code set to `0x0`, or an EOA had its nonce set to `2^64-1`. + +## Backwards Compatibility + +This EIP requires a protocol upgrade, since it modifies consensus rules. No existing contracts should be affected, as they will not have a nonce of `2^64-1`, nor will they have the contract code `0x1`. + +## Security Considerations + +The additional gas costs may cause potential DoS attacks if they access an arbitrary contract's data or make frequent contract deactivations. Contract authors must be aware and design contracts accordingly. There may be an effect on existing deployed code performing autonomous destruction and revival. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 533a1f89140151ab620218cad89bdff55d63e692 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 10 Jan 2023 11:08:15 -0500 Subject: [PATCH 156/274] Add EIP-6190: Functional SELFDESTRUCT (#6190) * Functional selfdestruct * Assign EIP-6190 * Rename eip-functional-selfdestruct.md to eip-6190.md * Add discussions url * Add RFC 8174 * Adjust some things to make SELFDESTRUCT work a bit smoother * Make verkle trees expected Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Add gas cost Rationale * Add proper Rationale * CREATE-based addresses Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-6190.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 EIPS/eip-6190.md diff --git a/EIPS/eip-6190.md b/EIPS/eip-6190.md new file mode 100644 index 00000000000000..e8f5aa9bfc46f7 --- /dev/null +++ b/EIPS/eip-6190.md @@ -0,0 +1,66 @@ +--- +eip: 6190 +title: Functional SELFDESTRUCT +description: Changes SELFDESTRUCT to only cause a finite number of state changes +author: Pandapip1 (@Pandapip1), Alex Beregszaszi (@axic) +discussions-to: https://ethereum-magicians.org/t/eip-6190-functional-selfdestruct/12232 +status: Draft +type: Standards Track +category: Core +created: 2022-12-20 +requires: 2929, 6188, 6189 +--- + +## Abstract + +Changes `SELFDESTRUCT` to only cause a finite number of state changes. + +## Motivation + +The `SELFDESTRUCT` instruction has a fixed price, but is unbounded in storage/account changes (it needs to delete all keys). This has been an outstanding concern for some time. + +Furthermore, with *Verkle trees* accounts will be organised differently. Account properties, including storage, would have individual keys. It would not be possible to traverse and find all used keys. This makes `SELFDESTRUCT` very challenging to support in Verkle trees. This EIP is a step towards supporting `SELFDESTRUCT` in Verkle trees. + +## 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 and RFC 8174. + +### Prerequisites + +[EIP-6188](./eip-6188.md) and [EIP-6189](./eip-6189.md) must be used for this EIP to function correctly. + +### `SELFDESTRUCT` Behaviour + +Instead of destroying the contract at the end of the transaction, instead, the following will occur at the end of the transaction in which it is invoked: + +1. The contract's code is set to `0x1`, and its nonce is set to `2^64-1`. +2. The contract's `0`th storage slot is set to the address that would be issued if the contract used the `CREATE` opcode (`keccak256(contractAddress, nonce)`). Note that the nonce is always `2^64-1`. +3. If the contract was self-destructed after the call being forwarded by one or more alias contracts, the alias contract's `0`th storage slot is set to the address calculated in step 2. +4. The contract's balance is transferred, in its entirety, to the address on the top of the stack. +5. The top of the stack is popped. + +### Gas Cost of `SELFDESTRUCT` + +The base gas cost of `SELFDESTRUCT` is set to `5000`. The gas cost of `SELFDESTRUCT` is increased by `5000` for each alias contract that forwarded to the contract being self-destructed. Finally, the [EIP-2929](./eip-2929.md) gas cost increase is applied. + +## Rationale + +This EIP is designed to be a step towards supporting `SELFDESTRUCT` in Verkle trees while making the minimum amount of changes. + +The `5000` base gas cost and additional alias contracts represents the cost of setting the account nonce and first storage slot. The [EIP-2929](./eip-2929.md) gas cost increase is preserved for the reasons mentioned in said EIP's Rationale. + +The nonce of `2^64-1` was chosen since it is the nonce protected by [EIP-6188](./eip-6188.md). The account code of `0x1` was chosen since it was the code specified in [EIP-6189](./eip-6189.md). + +The address being the same as the one created by `CREATE` is designed to reduce possible attack vectors by not introducing a new mechanism by which accounts can be created at specific addresses. + +## Backwards Compatibility + +This EIP requires a protocol upgrade, since it modifies consensus rules. + +## Security Considerations + +Needs discussion. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From fc2cefa3e8e8e448c34a308e7ee19ce5a0c51e63 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Wed, 11 Jan 2023 01:28:34 +0800 Subject: [PATCH 157/274] Add EIP-6147: Guard of NFT/SBT, an Extension of EIP-721 (#6147) * Create eip-draft_tittle_GONS.md * Update and rename eip-draft_tittle_GONS.md to eip-6147.md * Update eip-6147.md * Update eip-6147.md * Update eip-6147.md * Update eip-6147.md * Update eip-6147.md * Update eip-6147.md * Apply suggestions from code review Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-6147.md | 289 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 EIPS/eip-6147.md diff --git a/EIPS/eip-6147.md b/EIPS/eip-6147.md new file mode 100644 index 00000000000000..394dab6a54c06c --- /dev/null +++ b/EIPS/eip-6147.md @@ -0,0 +1,289 @@ +--- +eip: 6147 +title: Guard of NFT/SBT, an Extension of EIP-721 +description: A new management role of NFT/SBT is defined, which realizes the separation of transfer right and holding right of NFT/SBT. +author: 5660-eth (@5660-eth), Wizard Wang +discussions-to: https://ethereum-magicians.org/t/guard-of-nft-sbt-an-extension-of-eip-721/12052 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-07 +requires: 165, 721 +--- + +## Abstract + +This standard is an extension of [EIP-721](./eip-721.md). It separates the holding right and transfer right of non-fungible tokens (NFTs) and Soulbound Tokens (SBTs) and defines a new role, `guard`. The flexibility of the `guard` setting enables the design of NFT anti-theft, NFT lending, NFT leasing, SBT, etc. + +## Motivation + +NFTs are assets that have both use and financial value. + +Many cases of NFT theft currently exist, and current NFT anti-theft schemes, such as transferring NFTs to cold wallets, make NFTs inconvenient to be used. + +In current NFT lending, the NFT owner needs to transfer the NFT to the NFT lending contract, and the NFT owner no longer has the right to use the NFT while he or she has obtained the loan. In the real world, for example, if a person takes out a mortgage on his own house, he still has the right to use that house. + +For SBT, the current mainstream view is that an SBT is not transferable, which makes an SBT bound to an Ether address. However, when the private key of the user address is leaked or lost, retrieving SBT will become a complicated task and there is no corresponding specification. The SBTs essentially realizes the separation of NFT holding right and transfer right. When the wallet where SBT is located is stolen or unavailable, SBT should be able to be recoverable. + +In addition, SBTs still need to be managed in use. For example, if a university issues diploma SBTs to its graduates, and if the university later finds that a graduate has committed academic misconduct or jeopardized the reputation of the university, it should have the ability to retrieve the diploma SBT. + + +## 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. + +EIP-721 compliant contracts MAY implement this EIP. + +When a token has no guard, owner, authorised operators and approved address of the token MUST have permission to set guard. + +When a token has no guard, `guardOf` MUST return `address(0)`. + +When a token has a guard, owner, authorised operators and approved address of the token MUST NOT be able to change guard, and they MUST NOT be able to transfer the token. + +When a token has a guard, `guardOf` MUST return the address of the guard. + +When a token has a guard, the guard must be able to remove guard, change guard and transfer the token. + +When a token has a guard, if the token burns, the guard MUST be deleted. + +If issuing or minting SBTs, the guard MAY be uniformly set to the designated address to facilitate management. + +### Contract Interface + + +```solidity + interface IERC6147 { + + /// Logged when the guard of an NFT is changed + /// @notice Emitted when the `guard` is changed + /// The zero address for guard indicates that there is no guard address + event UpdateGuardLog(uint256 indexed tokenId,address indexed newGuard,address oldGuard); + + /// @notice Owner, authorised operators and approved address of the NFT can set guard of the NFT and guard can modifiy guard of the NFT + /// If the NFT has a guard role, the owner, authorised operators and approved address of the NFT cannot modify guard + /// @dev The newGuard can not be zero address + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to get the guard address for + /// @param newGuard The new guard address of the NFT + function changeGuard(uint256 tokenId, address newGuard) external; + + + /// @notice Remove the guard of the NFT + /// Only guard can remove its own guard role + /// @dev The guard address is set to 0 address + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to remove the guard address for + function removeGuard(uint256 tokenId) external; + + /// @notice Transfer the NFT and remove its guard role + /// @dev The NFT is transferred to `to` and the guard address is set to 0 address + /// Throws if `tokenId` is not valid NFT + /// @param from The address of the previous owner of the NFT + /// @param to The address of NFT recipient + /// @param tokenId The NFT to get transferred for + function transferAndRemove(address from,address to,uint256 tokenId) external; + + /// @notice Get the guard address of the NFT + /// @dev The zero address indicates that there is no guard + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to get the guard address for + /// @return The guard address for the NFT + function guardOf(uint256 tokenId) external view returns (address); +} + + ``` + +The `changeGuard(uint256 tokenId, address newGuard)` function MAY be implemented as `public` or `external`. + +The `removeGuard(uint256 tokenId)` function MAY be implemented as `public` or `external`. + +The `transferAndRemove(address from,address to,uint256 tokenId)` function MAY be implemented as `public` or `external`. + +The `guardOf(uint256 tokenId)` function MAY be implemented as `pure` or `view`. + +The `UpdateGuardLog` event MUST be emitted when a guard is changed. + +The `supportsInterface` method MUST return `true` when called with `0xc0655ef1`. + +## Rationale + +### Universality + +There are many application scenarios for NFT/SBT, and there is no need to propose a dedicated EIP for each one, which would make the overall number of EIPS inevitably increase and add to the burden of developers. The standard is based on the analysis of the right attached to assets in the real world, and abstracts the right attached to NFT/SBT into holding right and transfer right making the standard more universal. + +For example, the standard has and has more than the following use cases: + +SBTs. The SBTs issuer can assign a uniform role of `guard` to the SBTs before they are minted, so that the SBTs cannot be transferred by the corresponding holders and can be managed by the SBTs issuer through the `guard`. + +NFT anti-theft. If an NFT holder sets a `guard` address of an NFT as his or her own cold wallet address, the NFT can still be used by the NFT holder, but the risk of theft is greatly reduced. + +NFT lending. The borrower sets the `guard` of his or her own NFT as the lender's address, the borrower still has the right to use the NFT while obtaining the loan, but at the same time cannot transfer or sell the NFT. If the borrower defaults on the loan, the lender can transfer and sell the NFT. + +### Simplicity + +Improvements to the ETH protocol should be as simple as possible. Entities should not be multiplied beyond necessity. + +### Extensibility + +This standard only defines a `guard`, for the complex functions required by NFTs and SBTs, such as social recovery, multi-signature, expires management, according to the specific application scenarios, the `guard` can be set as a third-party protocol address, through the third-party protocol to achieve more flexible and diverse functions. + +### Naming + +The alternative names are `guardian` and `guard`, both of which basically match the permissions corresponding to the role: protection of NFT or necessary management according to its application scenarios. The `guard` has fewer characters than the `guardian` and is more concise. + +## Backwards Compatibility + +This standard can be fully EIP-721 compatible by adding an extension function set. + +If the NFT issued based on the above standard does not set a `guard` , then it is no different from the current NFT issued based on the EIP-721 standard. + + +## Reference Implementation + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.8; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "./IERC6147.sol"; + +abstract contract ERC721QS is ERC721, IERC6147 { + + mapping(uint256 => address) internal token_guard_map; + + /// @notice Update the guard of the NFT + /// @dev Delete function: set guard to 0 address,update function: set guard to new address + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to update the guard address for + /// @param newGuard The newGuard address + /// @param allowNull Allow 0 address + function updateGuard(uint256 tokenId,address newGuard,bool allowNull) internal { + address guard = guardOf(tokenId); + if (!allowNull) { + require(newGuard != address(0), "New guard can not be null"); + } + if (guard != address(0)) { + require(guard == _msgSender(), "only guard can change it self"); + } else { + require(_isApprovedOrOwner(_msgSender(), tokenId),"ERC721QS: caller is not owner nor approved"); + } + + if (guard != address(0) || newGuard != address(0)) { + token_guard_map[tokenId] = newGuard; + emit UpdateGuardLog(tokenId, newGuard, guard); + } + } + + /// @notice Owner sets guard or guard modifies guard + /// @dev The newGuard can not be zero address + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to get the guard address for + /// @param newGuard The new guard address of the NFT + function changeGuard(uint256 tokenId, address newGuard) public virtual{ + updateGuard(tokenId, newGuard, false); + } + + /// @notice Remove the guard of the NFT + /// @dev The guard address is set to 0 address + /// Only guard can remove its own guard role + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to remove the guard address for + function removeGuard(uint256 tokenId) public virtual { + updateGuard(tokenId, address(0), true); + } + + /// @notice Transfer the NFT and remove its guard role + /// Throws if `tokenId` is not valid NFT + /// @param from The address of the previous owner of the NFT + /// @param to The address of NFT recipient + /// @param tokenId The NFT to get transferred for + function transferAndRemove(address from,address to,uint256 tokenId) public virtual { + transferFrom(from,to,tokenId); + removeGuard(tokenId); + } + + /// @notice Get the guard address of the NFT + /// @dev The zero address indicates that there is no guard + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to get the guard address for + /// @return The guard address for the NFT + function guardOf(uint256 tokenId) public view virtual returns (address) { + return token_guard_map[tokenId]; + } + + /// @notice Check the guard address + /// @dev The zero address indicates there is no guard + /// Throws if `tokenId` is not valid NFT + /// @param tokenId The NFT to check the guard address for + /// @return The guard address + function checkGuard(uint256 tokenId) internal view returns (address) { + address guard = guardOf(tokenId); + address sender = _msgSender(); + if (guard != address(0)) { + require(guard == sender, "sender is not guard of token"); + return guard; + }else{ + return address(0); + } + } + + ///@dev When burning, delete `token_guard_map[tokenId]` + function _burn(uint256 tokenId) internal virtual override { + address guard=guardOf(tokenId); + super._burn(tokenId); + delete token_guard_map[tokenId]; + emit UpdateGuardLog(tokenId, address(0), guard); + } + + /// @dev Before transferring the NFT, need to check the gurard address + function transferFrom(address from,address to,uint256 tokenId) public virtual override { + address guard; + address new_from = from; + if (from != address(0)) { + guard = checkGuard(tokenId); + new_from = ownerOf(tokenId); + } + if (guard == address(0)) { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721: transfer caller is not owner nor approved" + ); + } + _transfer(new_from, to, tokenId); + } + + /// @dev Before safe transferring the NFT, need to check the gurard address + function safeTransferFrom(address from,address to,uint256 tokenId,bytes memory _data) public virtual override { + address guard; + address new_from = from; + if (from != address(0)) { + guard = checkGuard(tokenId); + new_from = ownerOf(tokenId); + } + if (guard == address(0)) { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC721: transfer caller is not owner nor approved" + ); + } + _safeTransfer(from, to, tokenId, _data); + } + + /// @dev See {IERC165-supportsInterface}. + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IERC721QS).interfaceId || super.supportsInterface(interfaceId); + } +} + +``` + + +## Security Considerations + +When an NFT has a `guard`, even if an address is authorized as an operator through `approve` or `setApprovalForAll`, the operator still has no right to transfer the NFT. + +For NFT trading platforms that trade through `setApprovalForAll` + holder's signature, when an NFT has a `guard`, it cannot be traded. It is recommended to prevent such pending orders by checking the interface beforehand. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From c550c5788b5bfa48bd65ade705dd1c56fb78ae7f Mon Sep 17 00:00:00 2001 From: Francisco Date: Tue, 10 Jan 2023 15:55:42 -0300 Subject: [PATCH 158/274] Update EIP-5267: Move to Last Call (#6300) * EIP-5267: Move to Last Call * lint --- EIPS/eip-5267.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5267.md b/EIPS/eip-5267.md index e26892def69231..c0b7722ab96c9c 100644 --- a/EIPS/eip-5267.md +++ b/EIPS/eip-5267.md @@ -4,7 +4,8 @@ title: Retrieval of EIP-712 domain description: A way to describe and retrieve an EIP-712 domain to securely integrate EIP-712 signatures. author: Francisco Giordano (@frangio) discussions-to: https://ethereum-magicians.org/t/eip-5267-retrieval-of-eip-712-domain/9951 -status: Review +status: Last Call +last-call-deadline: 2023-01-23 type: Standards Track category: ERC created: 2022-07-14 @@ -61,7 +62,7 @@ This is an optional extension to EIP-712 that does not introduce backwards compa Upgradeable contracts that make use of EIP-712 signatures MAY be upgraded to implement this EIP. -User-agents or applications that implement this EIP SHOULD additionally support those contracts that due to their immutability cannot be upgraded to implement this EIP, by hardcoding their domain based on contract address and chain id. +User-agents or applications that use this EIP SHOULD additionally support those contracts that due to their immutability cannot be upgraded to implement it. The simplest way to achieve this is to hardcode common domains based on contract address and chain id. However, it is also possible to implement a more general solution by guessing possible domains based on a few common patterns using the available information, and selecting the one whose hash matches a `DOMAIN_SEPARATOR` or `domainSeparator` function in the contract. ## Reference Implementation @@ -171,4 +172,5 @@ Additionally, the type of the `EIP712Domain` struct needs to be extended with th While this EIP allows a contract to specify a `verifyingContract` other than itself, as well as a `chainId` other than that of the current chain, user-agents and applications should in general validate that these do match the contract and chain before requesting any user signatures for the domain. This may not always be a valid assumption. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 0ede4d5d3d6a64a3f6bea2a4ed916d8a401f78d1 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Tue, 10 Jan 2023 13:10:35 -0800 Subject: [PATCH 159/274] Update EIP-5269 EIP/ERC Detection and Discovery (#6307) * Update * Update --- EIPS/eip-5269.md | 70 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index dab91003d8b624..b5e8bc62b9071d 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -1,6 +1,6 @@ --- eip: 5269 -title: ERC-identifying Interface +title: EIP/ERC Detection and Discovery description: An interface to identify if major behavior or optional behavior specified in an ERC is supported for a given caller. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc5269-human-readable-interface-detection/9957 @@ -8,9 +8,11 @@ status: Draft type: Standards Track category: ERC created: 2022-07-15 +requires: 5750 --- ## Abstract + An interface that returns true or false when queried by ERC numbers, if it implement certain ERC number. ## Motivation @@ -23,44 +25,84 @@ Here are the major differences between this EIP and [EIP-165](./eip-165.md). therefore it requires a method to *exist* in the first place. In some case, some ERCs interface are not represented in the way of method signature, such as some EIPs related to data format and signature schemes. 2. [EIP-165](./eip-165.md) doesn't provide query ability based on caller. This EIP respond `true` or `false` based on caller. -An example would be the OpenZeppelin has Transparency Proxy contract that behaves differently when -ProxyAdmin calls or other user calls. -3. Using ERC numbers improves human readability as well as make it easier to work with named contract such as ENS. + + +Here is the motivation for this EIP given EIP-165 already exists: + +1. Using EIP/ERC numbers improves human readability as well as make it easier to work with named contract such as ENS. + +2. Instead of using an EIP-165 identifier, we have seen an increasing interest for using EIP/ERC numbers as the way to identify or specify a EIP/ERCs. For example + +- [EIP-5267](./eip-5267.md) specifies `extensions` to be a list of EIP numbers. +- [EIP-600](./eip-600.md), [EIP-601](./eip-601.md) specifies EIP number in `m / purpose' / subpurpose' / EIP' / wallet'` +- [EIP-5568](./eip-5568.md) specifies `The instruction_id of an instruction defined by an EIP MUST be its EIP number unless there are exceptional circumstances (be reasonable)` +- [EIP-6120](./eip-6120.md) specifies `struct Token { uint eip; ..., }` where `uint eip` is an EIP number to identify EIPs. +- `EIP-867`(Stagnant) proposes to create `erpId: A string identifier for this ERP (likely the associated EIP number, e.g. “EIP-1234”).` + +3. Having a ERC/EIP number detection interface reduces the need for a lookup table in smart contract to +convert a function method or whole interface in any EIP/ERC in the bytes4 EIP-165 identifier into its respective EIP number and massively simplify the way to specify EIP for behavior expansion. + +4. We also recognize a smart contract might have different behavior given different caller. Most notable use case is that a common practice when using Transparent Upgradable Pattern is to give Admin and Non-Admin caller different treatment when calling a Proxy. ## Specification +In the following description we use EIP and ERC interexchangabily. This was because while most of the time the description applies to ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of queried. + 1. Any compliant smart contract MUST implement the following interface ```solidity interface IERC5269 { - function supportsErc(uint256 majorNumber, uint256 minorNumber, address caller) external view returns (boolean isSupported); + event OnSupportEIP( + bool state, // `true` means start supporting an EIP or some EIP behavior. `false` means stops supporting a EIP or some EIP behavior. + address indexed caller, // when emitted with `address(0x0)` means all callers. + uint256 indexed majorEIPIdentifier, + bytes32 indexed minorEIPIdentifier, // 0 means the entire EIP + bytes calldata extraData + ); + + /// @dev The core method of EIP/ERC Interface Detection + /// @param caller, a `address` value of the address of a caller being queried whether the given EIP is supported. + /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify. + /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](./eip-721.md) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. + /// @param extraData, a `bytes` for [EIP-5750](./eip-5750.md) for future extensions. + /// @returns magicWorld a bytes32 and SHOULD BE `bytes32 MAGIC_WORD = keccak256("EIP-5679")` if the contract supports an EIP and that behavior. + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external view returns (bytes32 magicWord); } ``` -2. `majorNumber` is the ERC number under query. `minorNumber = 0`is to reserved for the main interface. -Other `minorNumber` is reserved for the optional interface extension of that ERC. +In addition to the behavior specified in the comments of `IERC5269`: -3. Any compliant contract that is an `IERC5629` MUST return `true` for the call of `supportsErc(5269, 0, (any caller))`. -4. Any compliant standard is RECOMMENDED to declare the `minorNumber` for their optional interface extensions. +1. Any `minorEIPIdentifier=0` is reserved to be referring to the main behavior of the EIP being queried. +2. Authors of compliant EIP is RECOMMENDED to declare a list of `minorEIPIdentifier` for their optional interfaces, behaviors and value range for future extension. -5. Any compliant contract MUST return `true` when a behavior defined in that ERC and optional interface extensions is available to a` caller`. +3. Any compliant contract that is an `IERC5629` MUST return `MAGIC_WORD` for the call of `supportEIP((any caller), 5269, 0)`. +4. Any compliant contract SHOULD emit a `OnSupportEIP(true, address(0), 5269, 0, [])` +5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values. ## Rationale -1. EIP numbers are returned in an array to reflect the practice that a smart contract usually implements more than one interface. +1. When data type `uint256 majorEIPIdentifier`, there are other alternative options such as (1) use a hashed version of EIP number, or (2) use raw number, (3) use EIP-165 identifier. The pros for (1) is it automatically supports any evolvement of future EIP numbering/naming convention. But the cons is it's not backward readable: seeing a `hash(EIP-number)` one usually can't easily guess what EIP number is. We choose the (2) in the rationale laid out in motivation. +2. We have a `bytes32 minorEIPIdentifier`, alternatively it could be (1) a number, forcing all EIP author to define its numbering for subbehaviors so we go with a `bytes32` and asking EIP author to use a hash for a string name for their subbehaviors which they are already doing by coming up with interface name or method name in their specification. -2. We didn't require the ordering of return value. And we only suggest but didn't require deduplication because it's cheaper to do such computation outside of chain. +3. Alternatively it's possible we add extra data as return value or an array of all EIP being supported but we are unsure how much value this complexity brings. -3. Compared to [EIP-165](./eip-165.md), we also add an addition input of `address caller`, given the increasing popularity of proxy patterns such as those enabled by [EIP-1967](./eip-1967.md). One may ask: why not simply use `msg.sender`? This is because +4. Compared to [EIP-165](./eip-165.md), we also add an addition input of `address caller`, given the increasing popularity of proxy patterns such as those enabled by [EIP-1967](./eip-1967.md). One may ask: why not simply use `msg.sender`? This is because we want to allow query them without transaction or a proxy contract to query if whether interface ERC-`number` will be available to that particular sender. -4. We reserve the input `ercNumber` above 2**32 in case we need to support other collection of standards which are not ERC/EIP. +1. We reserve the input `majorEIPIdentifier` greater than or equals 2^32 in case we need to support other collection of standards which are not ERC/EIP. ## Security Considerations + Similar to [EIP-165](./eip-165.md) callers of the interface MUST assume the smart contract declaring they support such EIP interfaces doesn't necessarily correctly support them. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 76323946830d577ffd775505b1f1a2bfff7cde25 Mon Sep 17 00:00:00 2001 From: Keegan Lee Date: Wed, 11 Jan 2023 11:04:45 +0800 Subject: [PATCH 160/274] Update EIP-6150: Optimize and add more implementation (#6308) * Update EIP-6150: Optimize and add more implementation. * fix `EIP-N` check --- EIPS/eip-6150.md | 2 +- .../contracts/{ERC-6150.sol => ERC6150.sol} | 39 ++++++++++-- .../contracts/ERC6150AccessControl.sol | 62 +++++++++++++++++++ assets/eip-6150/contracts/ERC6150Burnable.sol | 32 ++++++++++ .../eip-6150/contracts/ERC6150Enumerable.sol | 40 ++++++++++++ .../contracts/ERC6150ParentTransferable.sol | 52 ++++++++++++++++ .../IERC6150.sol} | 11 +++- .../interfaces/IERC6150AccessControl.sol | 47 ++++++++++++++ .../contracts/interfaces/IERC6150Burnable.sol | 31 ++++++++++ .../interfaces/IERC6150Enumerable.sol | 42 +++++++++++++ .../interfaces/IERC6150ParentTransferable.sol | 40 ++++++++++++ 11 files changed, 391 insertions(+), 7 deletions(-) rename assets/eip-6150/contracts/{ERC-6150.sol => ERC6150.sol} (68%) create mode 100644 assets/eip-6150/contracts/ERC6150AccessControl.sol create mode 100644 assets/eip-6150/contracts/ERC6150Burnable.sol create mode 100644 assets/eip-6150/contracts/ERC6150Enumerable.sol create mode 100644 assets/eip-6150/contracts/ERC6150ParentTransferable.sol rename assets/eip-6150/contracts/{IERC-6150.sol => interfaces/IERC6150.sol} (84%) create mode 100644 assets/eip-6150/contracts/interfaces/IERC6150AccessControl.sol create mode 100644 assets/eip-6150/contracts/interfaces/IERC6150Burnable.sol create mode 100644 assets/eip-6150/contracts/interfaces/IERC6150Enumerable.sol create mode 100644 assets/eip-6150/contracts/interfaces/IERC6150ParentTransferable.sol diff --git a/EIPS/eip-6150.md b/EIPS/eip-6150.md index 279ea7ba531057..952a36f92c6754 100644 --- a/EIPS/eip-6150.md +++ b/EIPS/eip-6150.md @@ -280,7 +280,7 @@ This proposal is fully backward compatible with [EIP-721](./eip-721.md). ## Reference Implementation -Implementation: [EIP-6150.sol](../assets/eip-6150/contracts/ERC-6150.sol) +Implementation: [EIP-6150](../assets/eip-6150/contracts/ERC6150.sol) ## Security Considerations diff --git a/assets/eip-6150/contracts/ERC-6150.sol b/assets/eip-6150/contracts/ERC6150.sol similarity index 68% rename from assets/eip-6150/contracts/ERC-6150.sol rename to assets/eip-6150/contracts/ERC6150.sol index 11fdeb5a31c3b4..efac087ef95a1a 100644 --- a/assets/eip-6150/contracts/ERC-6150.sol +++ b/assets/eip-6150/contracts/ERC6150.sol @@ -1,11 +1,13 @@ +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; -import "./IERC-6150.sol"; +import "./interfaces/IERC6150.sol"; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; abstract contract ERC6150 is ERC721, IERC6150 { mapping(uint256 => uint256) private _parentOf; mapping(uint256 => uint256[]) private _childrenOf; + mapping(uint256 => uint256) private _indexInChildrenArray; constructor( string memory name_, @@ -33,7 +35,9 @@ abstract contract ERC6150 is ERC721, IERC6150 { function childrenOf( uint256 tokenId ) public view virtual override returns (uint256[] memory childrenIds) { - _requireMinted(tokenId); + if (tokenId > 0) { + _requireMinted(tokenId); + } childrenIds = _childrenOf[tokenId]; } @@ -51,6 +55,10 @@ abstract contract ERC6150 is ERC721, IERC6150 { return _childrenOf[tokenId].length == 0; } + function _getIndexInChildrenArray(uint256 tokenId) internal view virtual { + return _indexInChildrenArray[tokenId]; + } + function _safeBatchMintWithParent( address to, uint256 parentId, @@ -72,7 +80,7 @@ abstract contract ERC6150 is ERC721, IERC6150 { ) internal virtual { require( tokenIds.length == datas.length, - "EIP6150: tokenIds.length != datas.length" + "ERC6150: tokenIds.length != datas.length" ); for (uint256 i = 0; i < tokenIds.length; i++) { _safeMintWithParent(to, parentId, tokenIds[i], datas[i]); @@ -93,13 +101,14 @@ abstract contract ERC6150 is ERC721, IERC6150 { uint256 tokenId, bytes memory data ) internal virtual { - require(tokenId > 0, "EIP6150: tokenId is zero"); + require(tokenId > 0, "ERC6150: tokenId is zero"); if (parentId != 0) - require(_exists(parentId), "EIP6150: parentId doesn't exists"); + require(_exists(parentId), "ERC6150: parentId doesn't exist"); _beforeMintWithParent(to, parentId, tokenId); _parentOf[tokenId] = parentId; + _indexInChildrenArray[tokenId] = _childrenOf.length; _childrenOf[parentId].push(tokenId); _safeMint(to, tokenId, data); @@ -108,6 +117,26 @@ abstract contract ERC6150 is ERC721, IERC6150 { _afterMintWithParent(to, parentId, tokenId); } + function _safeBurn(uint256 tokenId) internal virtual { + require(_exists(tokenId), "ERC6150: tokenId doesn't exist"); + require(isLeaf(tokenId), "ERC6150: tokenId is not a leaf"); + + uint256 parent = _parentOf[tokenId]; + uint256 lastTokenIndex = _childrenOf[parent].length - 1; + uint256 targetTokenIndex = _indexInChildrenArray[tokenId]; + uint256 lastIndexToken = _childrenOf[parent][lastTokenIndex]; + if (lastTokenIndex > targetTokenIndex) { + _childrenOf[parent][targetTokenIndex] = lastIndexToken; + _indexInChildrenArray[lastIndexToken] = targetTokenIndex; + } + + delete _childrenOf[parent][lastIndexToken]; + delete _indexInChildrenArray[tokenId]; + delete _parentOf[tokenId]; + + _burn(tokenId); + } + function _beforeMintWithParent( address to, uint256 parentId, diff --git a/assets/eip-6150/contracts/ERC6150AccessControl.sol b/assets/eip-6150/contracts/ERC6150AccessControl.sol new file mode 100644 index 00000000000000..7182e374b395cf --- /dev/null +++ b/assets/eip-6150/contracts/ERC6150AccessControl.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC6150.sol"; +import "./interfaces/IERC6150AccessControl.sol"; + +abstract contract ERC6150AccessControl is ERC6150, IERC6150AccessControl { + mapping(address => mapping(uint256 => bool)) private _isAdminOf; + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6150) returns (bool) { + return + interfaceId == type(IERC6150AccessControl).interfaceId || + super.supportsInterface(interfaceId); + } + + function isAdminOf( + uint256 tokenId, + address account + ) public view virtual override returns (bool) { + return _isAdminOf[account][tokenId]; + } + + function canMintChildren( + uint256 parentId, + address account + ) public view virtual override returns (bool) { + return isAdminOf(parentId, account); + } + + function canBurnTokenByAccount( + uint256 tokenId, + address account + ) public view virtual override returns (bool) { + require(isLeaf(tokenId), "not a leaf token"); + return isAdminOf(tokenId, account); + } + + function _afterMintWithParent( + address to, + uint256 parentId, + uint256 tokenId + ) internal virtual override { + _isAdminOf[to][tokenId] = true; + } + + function _addAdmin(address admin, uint256 tokenId) internal virtual { + require(admin != address(0), "zero address"); + require(_exists(tokenId), "tokenId doesn't exist"); + _isAdminOf[admin][tokenId] = true; + } + + function _removeAdmin(address admin, uint256 tokenId) internal virtual { + require(_isAdminOf[admin][tokenId] == true, "not an admin"); + require(admin != ownerOf(tokenId), "cannot remove owner"); + _isAdminOf[admin][tokenId] = false; + } +} diff --git a/assets/eip-6150/contracts/ERC6150Burnable.sol b/assets/eip-6150/contracts/ERC6150Burnable.sol new file mode 100644 index 00000000000000..34f0d1eea77f33 --- /dev/null +++ b/assets/eip-6150/contracts/ERC6150Burnable.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC6150.sol"; +import "./interfaces/IERC6150Burnable.sol"; + +abstract contract ERC6150Burnable is ERC6150, IERC6150Burnable { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6150) returns (bool) { + return + interfaceId == type(IERC6150Burnable).interfaceId || + super.supportsInterface(interfaceId); + } + + function safeBurn(uint256 tokenId) public virtual override { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC6150Burnable: caller is neither token owner nor approved" + ); + _safeBurn(tokenId); + } + + function safeBatchBurn(uint256[] memory tokenIds) public virtual override { + for (uint256 i = 0; i < tokenIds.length; i++) { + safeBurn(tokenIds[i]); + } + } +} diff --git a/assets/eip-6150/contracts/ERC6150Enumerable.sol b/assets/eip-6150/contracts/ERC6150Enumerable.sol new file mode 100644 index 00000000000000..02751de61cf76f --- /dev/null +++ b/assets/eip-6150/contracts/ERC6150Enumerable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC6150.sol"; +import "./interfaces/IERC6150Enumerable.sol"; + +abstract contract ERC6150Enumerable is ERC6150, IERC6150Enumerable { + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6150) returns (bool) { + return + interfaceId == type(IERC6150Enumerable).interfaceId || + super.supportsInterface(interfaceId); + } + + function childrenCountOf( + uint256 parentId + ) external view virtual override returns (uint256) { + return childrenOf(parentId).length; + } + + function childOfParentByIndex( + uint256 parentId, + uint256 index + ) external view virtual override returns (uint256) { + uint256[] memory children = childrenOf(parentId); + return children[index]; + } + + function indexInChildrenEnumeration( + uint256 parentId, + uint256 tokenId + ) external view virtual override returns (uint256) { + require(parentOf(tokenId) == parentId, "wrong parent"); + return _getIndexInChildrenArray(tokenId); + } +} diff --git a/assets/eip-6150/contracts/ERC6150ParentTransferable.sol b/assets/eip-6150/contracts/ERC6150ParentTransferable.sol new file mode 100644 index 00000000000000..bb97f35a66bdfc --- /dev/null +++ b/assets/eip-6150/contracts/ERC6150ParentTransferable.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./ERC6150.sol"; +import "./interfaces/IERC6150ParentTransferable.sol"; + +abstract contract ERC6150ParentTransferable is + ERC6150, + IERC6150ParentTransferable +{ + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(IERC165, ERC6150) returns (bool) { + return + interfaceId == type(IERC6150ParentTransferable).interfaceId || + super.supportsInterface(interfaceId); + } + + function transferParent( + uint256 newParentId, + uint256 tokenId + ) public virtual override { + require( + _isApprovedOrOwner(_msgSender(), tokenId), + "ERC6150ParentTransferable: caller is not token owner nor approved" + ); + if (newParentId != 0) { + require( + _exists(newParentId), + "ERC6150ParentTransferable: newParentId doesn't exists" + ); + } + + uint256 owner = ownerOf(tokenId); + uint256 oldParentId = parentOf[tokenId]; + _safeBurn(tokenId); + _safeMintWithParent(owner, newParentId, tokenId); + emit ParentTransferred(tokenId, oldParentId, newParantId); + } + + function batchTransferParent( + uint256 newParentId, + uint256[] memory tokenIds + ) public virtual override { + for (uint256 i = 0; i < tokenIds.length; i++) { + transferParent(tokenIds[i], newParentId); + } + } +} diff --git a/assets/eip-6150/contracts/IERC-6150.sol b/assets/eip-6150/contracts/interfaces/IERC6150.sol similarity index 84% rename from assets/eip-6150/contracts/IERC-6150.sol rename to assets/eip-6150/contracts/interfaces/IERC6150.sol index d4830852a9b425..7da6ed559a868d 100644 --- a/assets/eip-6150/contracts/IERC-6150.sol +++ b/assets/eip-6150/contracts/interfaces/IERC6150.sol @@ -1,6 +1,15 @@ +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.0; -interface IERC6150 /* is IERC721, IERC165 */ { +import "IERC721.sol"; +import "IERC165.sol"; + +/** + * @title ERC-6150 Hierarchical NFTs Token Standard + * @dev See https://eips.ethereum.org/EIPS/eip-6150 + * Note: the ERC-165 identifier for this interface is 0x897e2c73. + */ +interface IERC6150 is IERC721, IERC165 { /** * @notice Emitted when `tokenId` token under `parentId` is minted. * @param minter The address of minter diff --git a/assets/eip-6150/contracts/interfaces/IERC6150AccessControl.sol b/assets/eip-6150/contracts/interfaces/IERC6150AccessControl.sol new file mode 100644 index 00000000000000..661679d446dd08 --- /dev/null +++ b/assets/eip-6150/contracts/interfaces/IERC6150AccessControl.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC6150.sol"; + +/** + * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for access control + * @dev See https://eips.ethereum.org/EIPS/eip-6150 + * Note: the ERC-165 identifier for this interface is 0x1d04f0b3. + */ +interface IERC6150AccessControl is IERC6150 { + /** + * @notice Check the account whether a admin of `tokenId` token. + * @dev Each token can be set more than one admin. Admin have permission to do something to the token, like mint child token, + * or burn token, or transfer parentship. + * @param tokenId The specified token + * @param account The account to be checked + * @return If the account has admin permission, return true; otherwise, return false. + */ + function isAdminOf( + uint256 tokenId, + address account + ) external view returns (bool); + + /** + * @notice Check whether the specified parent token and account can mint children tokens + * @dev If the `parentId` is zero, check whether account can mint root nodes + * @param parentId The specified parent token to be checked + * @param account The specified account to be checked + * @return If the token and account has mint permission, return true; otherwise, return false. + */ + function canMintChildren( + uint256 parentId, + address account + ) external view returns (bool); + + /** + * @notice Check whether the specified token can be burnt by specified account + * @param tokenId The specified token to be checked + * @param account The specified account to be checked + * @return If the tokenId can be burnt by account, return true; otherwise, return false. + */ + function canBurnTokenByAccount( + uint256 tokenId, + address account + ) external view returns (bool); +} diff --git a/assets/eip-6150/contracts/interfaces/IERC6150Burnable.sol b/assets/eip-6150/contracts/interfaces/IERC6150Burnable.sol new file mode 100644 index 00000000000000..5cb8c9a302e0e9 --- /dev/null +++ b/assets/eip-6150/contracts/interfaces/IERC6150Burnable.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC6150.sol"; + +/** + * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for burnable + * @dev See https://eips.ethereum.org/EIPS/eip-6150 + * Note: the ERC-165 identifier for this interface is 0x4ac0aa46. + */ +interface IERC6150Burnable is IERC6150 { + /** + * @notice Burn the `tokenId` token. + * @dev Throws if `tokenId` is not a leaf token. + * Throws if `tokenId` is not a valid NFT. + * Throws if `owner` is not the owner of `tokenId` token. + * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this token. + * @param tokenId The token to be burnt + */ + function safeBurn(uint256 tokenId) external; + + /** + * @notice Batch burn tokens. + * @dev Throws if one of `tokenIds` is not a leaf token. + * Throws if one of `tokenIds` is not a valid NFT. + * Throws if `owner` is not the owner of all `tokenIds` tokens. + * Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for all `tokenIds`. + * @param tokenIds The tokens to be burnt + */ + function safeBatchBurn(uint256[] memory tokenIds) external; +} diff --git a/assets/eip-6150/contracts/interfaces/IERC6150Enumerable.sol b/assets/eip-6150/contracts/interfaces/IERC6150Enumerable.sol new file mode 100644 index 00000000000000..030e4d9b3dbdb4 --- /dev/null +++ b/assets/eip-6150/contracts/interfaces/IERC6150Enumerable.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC6150.sol"; +import "IERC721Enumerable.sol"; + +/** + * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for enumerable + * @dev See https://eips.ethereum.org/EIPS/eip-6150 + * Note: the ERC-165 identifier for this interface is 0xba541a2e. + */ +interface IERC6150Enumerable is IERC6150, IERC721Enumerable { + /** + * @notice Get total amount of children tokens under `parentId` token. + * @dev If `parentId` is zero, it means get total amount of root tokens. + * @return The total amount of children tokens under `parentId` token. + */ + function childrenCountOf(uint256 parentId) external view returns (uint256); + + /** + * @notice Get the token at the specified index of all children tokens under `parentId` token. + * @dev If `parentId` is zero, it means get root token. + * @return The token ID at `index` of all chlidren tokens under `parentId` token. + */ + function childOfParentByIndex( + uint256 parentId, + uint256 index + ) external view returns (uint256); + + /** + * @notice Get the index position of specified token in the children enumeration under specified parent token. + * @dev Throws if the `tokenId` is not found in the children enumeration. + * If `parentId` is zero, means get root token index. + * @param parentId The parent token + * @param tokenId The specified token to be found + * @return The index position of `tokenId` found in the children enumeration + */ + function indexInChildrenEnumeration( + uint256 parentId, + uint256 tokenId + ) external view returns (uint256); +} diff --git a/assets/eip-6150/contracts/interfaces/IERC6150ParentTransferable.sol b/assets/eip-6150/contracts/interfaces/IERC6150ParentTransferable.sol new file mode 100644 index 00000000000000..2ecfa97f61dce0 --- /dev/null +++ b/assets/eip-6150/contracts/interfaces/IERC6150ParentTransferable.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +import "./IERC6150.sol"; + +/** + * @title ERC-6150 Hierarchical NFTs Token Standard, optional extension for parent transferable + * @dev See https://eips.ethereum.org/EIPS/eip-6150 + * Note: the ERC-165 identifier for this interface is 0xfa574808. + */ +interface IERC6150ParentTransferable is IERC6150 { + /** + * @notice Emitted when the parent of `tokenId` token changed. + * @param tokenId The token changed + * @param oldParentId Previous parent token + * @param newParentId New parent token + */ + event ParentTransferred( + uint256 tokenId, + uint256 oldParentId, + uint256 newParentId + ); + + /** + * @notice Transfer parentship of `tokenId` token to a new parent token + * @param newParentId New parent token id + * @param tokenId The token to be changed + */ + function transferParent(uint256 newParentId, uint256 tokenId) external; + + /** + * @notice Batch transfer parentship of `tokenIds` to a new parent token + * @param newParentId New parent token id + * @param tokenIds Array of token ids to be changed + */ + function batchTransferParent( + uint256 newParentId, + uint256[] memory tokenIds + ) external; +} From 5cb167fe34d41993992cf936d8ccacf5d3de112f Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Wed, 11 Jan 2023 13:52:36 -0500 Subject: [PATCH 161/274] Update EIP-6190: Rename to verkle-compatible SELFDESTRUCT (#6313) --- EIPS/eip-6190.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6190.md b/EIPS/eip-6190.md index e8f5aa9bfc46f7..90fddc4871cfa4 100644 --- a/EIPS/eip-6190.md +++ b/EIPS/eip-6190.md @@ -1,6 +1,6 @@ --- eip: 6190 -title: Functional SELFDESTRUCT +title: Verkle-compatible SELFDESTRUCT description: Changes SELFDESTRUCT to only cause a finite number of state changes author: Pandapip1 (@Pandapip1), Alex Beregszaszi (@axic) discussions-to: https://ethereum-magicians.org/t/eip-6190-functional-selfdestruct/12232 From 57a14f9fb9982894f81248046d2e45d2ecfdf14e Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 11 Jan 2023 10:56:41 -0800 Subject: [PATCH 162/274] Update EIP-5629 about DRAFT identifier (#6312) * Update * Update * Move to review * Update * Update * update * update --- EIPS/eip-5269.md | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index b5e8bc62b9071d..65d78dadf77369 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -46,17 +46,17 @@ convert a function method or whole interface in any EIP/ERC in the bytes4 EIP-16 ## Specification -In the following description we use EIP and ERC interexchangabily. This was because while most of the time the description applies to ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of queried. +In the following description we use EIP and ERC inter-exchangebly. This was because while most of the time the description applies to ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of queried. 1. Any compliant smart contract MUST implement the following interface ```solidity interface IERC5269 { event OnSupportEIP( - bool state, // `true` means start supporting an EIP or some EIP behavior. `false` means stops supporting a EIP or some EIP behavior. address indexed caller, // when emitted with `address(0x0)` means all callers. uint256 indexed majorEIPIdentifier, bytes32 indexed minorEIPIdentifier, // 0 means the entire EIP + bytes32 eipStatus, bytes calldata extraData ); @@ -65,30 +65,38 @@ interface IERC5269 { /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify. /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](./eip-721.md) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. /// @param extraData, a `bytes` for [EIP-5750](./eip-5750.md) for future extensions. - /// @returns magicWorld a bytes32 and SHOULD BE `bytes32 MAGIC_WORD = keccak256("EIP-5679")` if the contract supports an EIP and that behavior. + /// @returns eipStatus a bytes32 indicating the status of EIP the contract supports. + /// - For FINAL EIPs, it MUST return `keccak256("FINAL")`. + /// - For non-FINAL EIPs, it SHOULD return `keccak256("DRAFT")`. + /// During EIP procedure, EIP authors are allowed to specify their own + /// eipStatus other than `FINAL` or `DRAFT` at their discretion such as `keccak256("DRAFTv1")` + /// or `keccak256("DRAFT-option1")`and such value of eipStatus MUST be documented in the EIP body function supportEIP( address caller, uint256 majorEIPIdentifier, bytes32 minorEIPIdentifier, bytes calldata extraData) - external view returns (bytes32 magicWord); + external view returns (bytes32 eipStatus); } ``` In addition to the behavior specified in the comments of `IERC5269`: - 1. Any `minorEIPIdentifier=0` is reserved to be referring to the main behavior of the EIP being queried. 2. Authors of compliant EIP is RECOMMENDED to declare a list of `minorEIPIdentifier` for their optional interfaces, behaviors and value range for future extension. +3. When this EIP is FINAL, any compliant contract that is an `IERC5629` MUST return `keccak256("FINAL")` for the call of `supportEIP((any caller), 5269, 0, [])`. + +*Note*: at the current snapshot, `supportEIP((any caller), 5269, 0, [])` MUST return `keccak256("DRAFT")`. -3. Any compliant contract that is an `IERC5629` MUST return `MAGIC_WORD` for the call of `supportEIP((any caller), 5269, 0)`. 4. Any compliant contract SHOULD emit a `OnSupportEIP(true, address(0), 5269, 0, [])` 5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values. +6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`. +7. The `supportEIP` MUST be `view`, ie. it MUST NOT mutate any global state of EVM. ## Rationale 1. When data type `uint256 majorEIPIdentifier`, there are other alternative options such as (1) use a hashed version of EIP number, or (2) use raw number, (3) use EIP-165 identifier. The pros for (1) is it automatically supports any evolvement of future EIP numbering/naming convention. But the cons is it's not backward readable: seeing a `hash(EIP-number)` one usually can't easily guess what EIP number is. We choose the (2) in the rationale laid out in motivation. -2. We have a `bytes32 minorEIPIdentifier`, alternatively it could be (1) a number, forcing all EIP author to define its numbering for subbehaviors so we go with a `bytes32` and asking EIP author to use a hash for a string name for their subbehaviors which they are already doing by coming up with interface name or method name in their specification. +2. We have a `bytes32 minorEIPIdentifier`, alternatively it could be (1) a number, forcing all EIP author to define its numbering for sub-behaviors so we go with a `bytes32` and asking EIP author to use a hash for a string name for their sub-behaviors which they are already doing by coming up with interface name or method name in their specification. 3. Alternatively it's possible we add extra data as return value or an array of all EIP being supported but we are unsure how much value this complexity brings. From 118a5a0e9c1956a909e43d8749ff140a4e05468a Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 11 Jan 2023 22:14:48 -0800 Subject: [PATCH 163/274] Update EIP-5269 (#6316) * Update * Update * Move to review * Update * Update * update * update * Add RefImpl * Fix * Update * Update * Update * Update * Update --- EIPS/eip-5269.md | 161 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 155 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index 65d78dadf77369..d10af7da99eb9f 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -13,7 +13,16 @@ requires: 5750 ## Abstract -An interface that returns true or false when queried by ERC numbers, if it implement certain ERC number. +An interface for better identification and detection of EIP/ERC by numbers. + +It designate a field in which it's called `majorEIPIdentifier` which is normally known or referred to as "EIP number". For example, `ERC-721` aka [EIP-721](./eip-721.md) has a `majorEIPIdentifier = 721`. This EIP has a `majorEIPIdentifier = 5269`. +Calling it as "majorEIPIdentifier" instead of EIP number to make it future proof: anticipating there is a possibility where future EIP is not numbered or if we want to +incorporate other type of standards. +It also proposes a new concept of `minorEIPIdentifier` which is left for authors of +individual EIP to define. For example, EIP-721's author may define `ERC721Metadata` +interface as `minorEIPIdentifier= keccak256("ERC721Metadata")`. + +It also proposes an event to allow contract to optionally declare EIPs they support. ## Motivation @@ -51,21 +60,24 @@ In the following description we use EIP and ERC inter-exchangebly. This was beca 1. Any compliant smart contract MUST implement the following interface ```solidity +// DRAFTv1 +pragma solidity ^0.8.9; + interface IERC5269 { event OnSupportEIP( address indexed caller, // when emitted with `address(0x0)` means all callers. uint256 indexed majorEIPIdentifier, bytes32 indexed minorEIPIdentifier, // 0 means the entire EIP bytes32 eipStatus, - bytes calldata extraData + bytes extraData ); /// @dev The core method of EIP/ERC Interface Detection /// @param caller, a `address` value of the address of a caller being queried whether the given EIP is supported. /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify. - /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](./eip-721.md) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. - /// @param extraData, a `bytes` for [EIP-5750](./eip-5750.md) for future extensions. - /// @returns eipStatus a bytes32 indicating the status of EIP the contract supports. + /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](/EIPS/eip-721) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. + /// @param extraData, a `bytes` for [EIP-5750](/EIPS/eip-5750) for future extensions. + /// @return eipStatus a bytes32 indicating the status of EIP the contract supports. /// - For FINAL EIPs, it MUST return `keccak256("FINAL")`. /// - For non-FINAL EIPs, it SHOULD return `keccak256("DRAFT")`. /// During EIP procedure, EIP authors are allowed to specify their own @@ -104,7 +116,144 @@ In addition to the behavior specified in the comments of `IERC5269`: we want to allow query them without transaction or a proxy contract to query if whether interface ERC-`number` will be available to that particular sender. -1. We reserve the input `majorEIPIdentifier` greater than or equals 2^32 in case we need to support other collection of standards which are not ERC/EIP. +5. We reserve the input `majorEIPIdentifier` greater than or equals 2^32 in case we need to support other collection of standards which are not ERC/EIP. + +## Test Cases + +Here is an example of test cases. +See Reference Implementation section for a reference implementation being tested. + +```typescript + +describe("ERC5269", function () { + async function deployFixture() { + // ... + } + + describe("Deployment", function () { + // ... + it("Should emit proper OnSupportEIP events", async function () { + let { txDeployErc721 } = await loadFixture(deployFixture); + let events = txDeployErc721.events?.filter(event => event.event === 'OnSupportEIP'); + expect(events).to.have.lengthOf(4); + + let ev5269 = events!.filter( + (event) => event.args!.majorEIPIdentifier.eq(5269)); + expect(ev5269).to.have.lengthOf(1); + expect(ev5269[0].args!.caller).to.equal(BigNumber.from(0)); + expect(ev5269[0].args!.minorEIPIdentifier).to.equal(BigNumber.from(0)); + expect(ev5269[0].args!.eipStatus).to.equal(ethers.utils.id("DRAFTv1")); + + let ev721 = events!.filter( + (event) => event.args!.majorEIPIdentifier.eq(721)); + expect(ev721).to.have.lengthOf(3); + expect(ev721[0].args!.caller).to.equal(BigNumber.from(0)); + expect(ev721[0].args!.minorEIPIdentifier).to.equal(BigNumber.from(0)); + expect(ev721[0].args!.eipStatus).to.equal(ethers.utils.id("FINAL")); + + expect(ev721[1].args!.caller).to.equal(BigNumber.from(0)); + expect(ev721[1].args!.minorEIPIdentifier).to.equal(ethers.utils.id("ERC721Metadata")); + expect(ev721[1].args!.eipStatus).to.equal(ethers.utils.id("FINAL")); + + // ... + }); + + it("Should return proper eipStatus value when called supportEIP() for declared supported EIP/features", async function () { + let { erc721ForTesting, owner } = await loadFixture(deployFixture); + expect(await erc721ForTesting.supportEIP(owner.address, 5269, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("DRAFTv1")); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("FINAL")); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("ERC721Metadata"), [])).to.equal(ethers.utils.id("FINAL")); + // ... + + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0)); + expect(await erc721ForTesting.supportEIP(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0)); + }); + + it("Should return zero as eipStatus value when called supportEIP() for non declared EIP/features", async function () { + let { erc721ForTesting, owner } = await loadFixture(deployFixture); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0)); + expect(await erc721ForTesting.supportEIP(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0)); + }); + }); +}); +``` + +## Reference Implementation + +Here is a reference implementation for this EIP: + +```solidity + +contract ERC5269 is IERC5269 { + bytes32 constant public EIP_STATUS = keccak256("DRAFTv1"); + constructor () { + emit OnSupportEIP(address(0x0), 5269, bytes32(0), EIP_STATUS, ""); + } + + function _supportEIP( + address /*caller*/, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata /*extraData*/) + internal virtual view returns (bytes32 eipStatus) { + if (majorEIPIdentifier == 5269) { + if (minorEIPIdentifier == bytes32(0)) { + return EIP_STATUS; + } + } + return bytes32(0); + } + + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external virtual view returns (bytes32 eipStatus) { + return _supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData); + } +} +``` + +Here is an example where a contract of [EIP-721](./eip-721.md) also implement this EIP to make it easier +to detect and discover: + +```solidity +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "../ERC5269.sol"; +contract ERC721ForTesting is ERC721, ERC5269 { + + bytes32 constant public EIP_FINAL = keccak256("FINAL"); + constructor() ERC721("ERC721ForTesting", "E721FT") ERC5269() { + _mint(msg.sender, 0); + emit OnSupportEIP(address(0x0), 721, bytes32(0), EIP_FINAL, ""); + emit OnSupportEIP(address(0x0), 721, keccak256("ERC721Metadata"), EIP_FINAL, ""); + emit OnSupportEIP(address(0x0), 721, keccak256("ERC721Enumerable"), EIP_FINAL, ""); + } + + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external + override + view + returns (bytes32 eipStatus) { + if (majorEIPIdentifier == 721) { + if (minorEIPIdentifier == 0) { + return keccak256("FINAL"); + } else if (minorEIPIdentifier == keccak256("ERC721Metadata")) { + return keccak256("FINAL"); + } else if (minorEIPIdentifier == keccak256("ERC721Enumerable")) { + return keccak256("FINAL"); + } + } + return super._supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData); + } +} + +``` ## Security Considerations From 1df2353b14dd0860d3d0a8deddf4d03707b8be7d Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 11 Jan 2023 22:37:02 -0800 Subject: [PATCH 164/274] Add asset file for EIP-5269 (#6317) * Add asset file for eip-5269 * Add asset file for eip-5269 --- EIPS/eip-5269.md | 2 +- assets/eip-5269/contracts/ERC5269.sol | 39 ++++++++ assets/eip-5269/contracts/IERC5269.sol | 34 +++++++ .../contracts/testing/ERC721ForTesting.sol | 42 +++++++++ assets/eip-5269/test/TestERC5269.ts | 88 +++++++++++++++++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 assets/eip-5269/contracts/ERC5269.sol create mode 100644 assets/eip-5269/contracts/IERC5269.sol create mode 100644 assets/eip-5269/contracts/testing/ERC721ForTesting.sol create mode 100644 assets/eip-5269/test/TestERC5269.ts diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index d10af7da99eb9f..b6eab38a830020 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -102,7 +102,7 @@ In addition to the behavior specified in the comments of `IERC5269`: 4. Any compliant contract SHOULD emit a `OnSupportEIP(true, address(0), 5269, 0, [])` 5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values. -6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`. +6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`, it is RECOMMENDED to return `0`. 7. The `supportEIP` MUST be `view`, ie. it MUST NOT mutate any global state of EVM. ## Rationale diff --git a/assets/eip-5269/contracts/ERC5269.sol b/assets/eip-5269/contracts/ERC5269.sol new file mode 100644 index 00000000000000..4d89b471c9767e --- /dev/null +++ b/assets/eip-5269/contracts/ERC5269.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: CC0-1.0 +// Author: Zainan Victor Zhou +// DRAFTv1 +// Source https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5269 +// Deployment https://goerli.etherscan.io/address/0x33F735852619E3f99E1AF069cCf3b9232b2806bE#code + +pragma solidity ^0.8.9; + +import "./IERC5269.sol"; + +contract ERC5269 is IERC5269 { + bytes32 constant public EIP_STATUS = keccak256("DRAFTv1"); + constructor () { + emit OnSupportEIP(address(0x0), 5269, bytes32(0), EIP_STATUS, ""); + } + + function _supportEIP( + address /*caller*/, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata /*extraData*/) + internal virtual view returns (bytes32 eipStatus) { + if (majorEIPIdentifier == 5269) { + if (minorEIPIdentifier == bytes32(0)) { + return EIP_STATUS; + } + } + return bytes32(0); + } + + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external virtual view returns (bytes32 eipStatus) { + return _supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData); + } +} diff --git a/assets/eip-5269/contracts/IERC5269.sol b/assets/eip-5269/contracts/IERC5269.sol new file mode 100644 index 00000000000000..8b444549578895 --- /dev/null +++ b/assets/eip-5269/contracts/IERC5269.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: CC0-1.0 +// Author: Zainan Victor Zhou +// DRAFTv1 +// Source https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5269 +// Deployment https://goerli.etherscan.io/address/0x33F735852619E3f99E1AF069cCf3b9232b2806bE#code +pragma solidity ^0.8.9; + +interface IERC5269 { + event OnSupportEIP( + address indexed caller, // when emitted with `address(0x0)` means all callers. + uint256 indexed majorEIPIdentifier, + bytes32 indexed minorEIPIdentifier, // 0 means the entire EIP + bytes32 eipStatus, + bytes extraData + ); + + /// @dev The core method of EIP/ERC Interface Detection + /// @param caller, a `address` value of the address of a caller being queried whether the given EIP is supported. + /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify. + /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](/EIPS/eip-721) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. + /// @param extraData, a `bytes` for [EIP-5750](/EIPS/eip-5750) for future extensions. + /// @return eipStatus a bytes32 indicating the status of EIP the contract supports. + /// - For FINAL EIPs, it MUST return `keccak256("FINAL")`. + /// - For non-FINAL EIPs, it SHOULD return `keccak256("DRAFT")`. + /// During EIP procedure, EIP authors are allowed to specify their own + /// eipStatus other than `FINAL` or `DRAFT` at their discretion such as `keccak256("DRAFTv1")` + /// or `keccak256("DRAFT-option1")`and such value of eipStatus MUST be documented in the EIP body + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external view returns (bytes32 eipStatus); +} diff --git a/assets/eip-5269/contracts/testing/ERC721ForTesting.sol b/assets/eip-5269/contracts/testing/ERC721ForTesting.sol new file mode 100644 index 00000000000000..22d21c469b9c4f --- /dev/null +++ b/assets/eip-5269/contracts/testing/ERC721ForTesting.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: CC0-1.0 +// Author: Zainan Victor Zhou +// DRAFTv1 +// Source https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5269 +// Deployment https://goerli.etherscan.io/address/0x33F735852619E3f99E1AF069cCf3b9232b2806bE#code +pragma solidity ^0.8.9; +// import 721 +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +// impport 5269 +import "../ERC5269.sol"; + +contract ERC721ForTesting is ERC721, ERC5269 { + + bytes32 constant public EIP_FINAL = keccak256("FINAL"); + constructor() ERC721("ERC721ForTesting", "E721FT") ERC5269() { + _mint(msg.sender, 0); + emit OnSupportEIP(address(0x0), 721, bytes32(0), EIP_FINAL, ""); + emit OnSupportEIP(address(0x0), 721, keccak256("ERC721Metadata"), EIP_FINAL, ""); + emit OnSupportEIP(address(0x0), 721, keccak256("ERC721Enumerable"), EIP_FINAL, ""); + } + + function supportEIP( + address caller, + uint256 majorEIPIdentifier, + bytes32 minorEIPIdentifier, + bytes calldata extraData) + external + override + view + returns (bytes32 eipStatus) { + if (majorEIPIdentifier == 721) { + if (minorEIPIdentifier == 0) { + return keccak256("FINAL"); + } else if (minorEIPIdentifier == keccak256("ERC721Metadata")) { + return keccak256("FINAL"); + } else if (minorEIPIdentifier == keccak256("ERC721Enumerable")) { + return keccak256("FINAL"); + } + } + return super._supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData); + } +} diff --git a/assets/eip-5269/test/TestERC5269.ts b/assets/eip-5269/test/TestERC5269.ts new file mode 100644 index 00000000000000..b71635df22b2af --- /dev/null +++ b/assets/eip-5269/test/TestERC5269.ts @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: CC0-1.0 +// Author: Zainan Victor Zhou +// DRAFTv1 +// Source https://github.com/ercref/ercref-contracts/tree/main/ERCs/eip-5269 +// Deployment https://goerli.etherscan.io/address/0x33F735852619E3f99E1AF069cCf3b9232b2806bE#code + +import { loadFixture, mine } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { BigNumber, ContractReceipt, Wallet } from "ethers"; +import { ethers } from "hardhat"; + +describe("ERC5269", function () { + async function deployFixture() { + // Contracts are deployed using the first signer/account by default + const [owner, mintSender, recipient] = await ethers.getSigners(); + const testWallet: Wallet = new ethers.Wallet("0x0000000000000000000000000000000000000000000000000000000000000001"); + + const factory = await ethers.getContractFactory("ERC5269"); + const contract = await factory.deploy(); + let tx1 = await contract.deployed(); + let txDeployErc5269: ContractReceipt = await tx1.deployTransaction.wait(); + + const ERC721ForTesting = await ethers.getContractFactory("ERC721ForTesting"); + const erc721ForTesting = await ERC721ForTesting.deploy(); + let tx2 = await erc721ForTesting.deployed(); + const txDeployErc721: ContractReceipt = await tx2.deployTransaction.wait(); + const provider = ethers.provider; + return { + provider, + contract, + erc721ForTesting, + tx1, txDeployErc5269, + tx2, txDeployErc721, + owner, mintSender, recipient, testWallet + }; + } + + describe("Deployment", function () { + it("Should be deployable", async function () { + await loadFixture(deployFixture); + }); + + it("Should emit proper OnSupportEIP events", async function () { + let { txDeployErc721 } = await loadFixture(deployFixture); + let events = txDeployErc721.events?.filter(event => event.event === 'OnSupportEIP'); + expect(events).to.have.lengthOf(4); + + let ev5269 = events!.filter( + (event) => event.args!.majorEIPIdentifier.eq(5269)); + expect(ev5269).to.have.lengthOf(1); + expect(ev5269[0].args!.caller).to.equal(BigNumber.from(0)); + expect(ev5269[0].args!.minorEIPIdentifier).to.equal(BigNumber.from(0)); + expect(ev5269[0].args!.eipStatus).to.equal(ethers.utils.id("DRAFTv1")); + + let ev721 = events!.filter( + (event) => event.args!.majorEIPIdentifier.eq(721)); + expect(ev721).to.have.lengthOf(3); + expect(ev721[0].args!.caller).to.equal(BigNumber.from(0)); + expect(ev721[0].args!.minorEIPIdentifier).to.equal(BigNumber.from(0)); + expect(ev721[0].args!.eipStatus).to.equal(ethers.utils.id("FINAL")); + + expect(ev721[1].args!.caller).to.equal(BigNumber.from(0)); + expect(ev721[1].args!.minorEIPIdentifier).to.equal(ethers.utils.id("ERC721Metadata")); + expect(ev721[1].args!.eipStatus).to.equal(ethers.utils.id("FINAL")); + + expect(ev721[2].args!.caller).to.equal(BigNumber.from(0)); + expect(ev721[2].args!.minorEIPIdentifier).to.equal(ethers.utils.id("ERC721Enumerable")); + expect(ev721[2].args!.eipStatus).to.equal(ethers.utils.id("FINAL")); + }); + + it("Should return proper eipStatus value when called supportEIP() for declared supported EIP/features", async function () { + let { erc721ForTesting, owner } = await loadFixture(deployFixture); + expect(await erc721ForTesting.supportEIP(owner.address, 5269, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("DRAFTv1")); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("FINAL")); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("ERC721Metadata"), [])).to.equal(ethers.utils.id("FINAL")); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("ERC721Enumerable"), [])).to.equal(ethers.utils.id("FINAL")); + + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0)); + expect(await erc721ForTesting.supportEIP(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0)); + }); + + it("Should return zero as eipStatus value when called supportEIP() for non declared EIP/features", async function () { + let { erc721ForTesting, owner } = await loadFixture(deployFixture); + expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0)); + expect(await erc721ForTesting.supportEIP(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0)); + }); + }); +}); From 46fc45f6a6e3692cfaf8105637a0c94ff4da0549 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 11 Jan 2023 22:44:18 -0800 Subject: [PATCH 165/274] Add asset file for eip-5269 (#6318) --- EIPS/eip-5269.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index b6eab38a830020..4eb7f537868273 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -92,6 +92,8 @@ interface IERC5269 { } ``` +See [`IERC5269.sol`](../assets/eip-5269/contracts/IERC5269.sol). + In addition to the behavior specified in the comments of `IERC5269`: 1. Any `minorEIPIdentifier=0` is reserved to be referring to the main behavior of the EIP being queried. @@ -178,6 +180,8 @@ describe("ERC5269", function () { }); ``` +See [`TestERC5269.ts`](../assets/eip-5269/test/TestERC5269.ts). + ## Reference Implementation Here is a reference implementation for this EIP: @@ -215,6 +219,8 @@ contract ERC5269 is IERC5269 { } ``` +See [`ERC5269.sol`](../assets/eip-5269/contracts/ERC5269.sol). + Here is an example where a contract of [EIP-721](./eip-721.md) also implement this EIP to make it easier to detect and discover: @@ -255,6 +261,8 @@ contract ERC721ForTesting is ERC721, ERC5269 { ``` +See [`ERC721ForTesting.sol`](../assets/eip-5269/contracts/testing/ERC721ForTesting.sol). + ## Security Considerations Similar to [EIP-165](./eip-165.md) callers of the interface MUST assume the smart contract From 1066169cd6a07bd1ef2394acfdfb5390282a18c2 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Wed, 11 Jan 2023 23:02:45 -0800 Subject: [PATCH 166/274] Update EIP-5269 (#6319) * Update * Merge --- EIPS/eip-5269.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index 4eb7f537868273..9896ac0d03a2b6 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -16,8 +16,10 @@ requires: 5750 An interface for better identification and detection of EIP/ERC by numbers. It designate a field in which it's called `majorEIPIdentifier` which is normally known or referred to as "EIP number". For example, `ERC-721` aka [EIP-721](./eip-721.md) has a `majorEIPIdentifier = 721`. This EIP has a `majorEIPIdentifier = 5269`. -Calling it as "majorEIPIdentifier" instead of EIP number to make it future proof: anticipating there is a possibility where future EIP is not numbered or if we want to + +Calling it as "majorEIPIdentifier" instead of "EIP number" makes it future proof: anticipating there is a possibility where future EIP is not numbered or if we want to incorporate other type of standards. + It also proposes a new concept of `minorEIPIdentifier` which is left for authors of individual EIP to define. For example, EIP-721's author may define `ERC721Metadata` interface as `minorEIPIdentifier= keccak256("ERC721Metadata")`. @@ -77,7 +79,7 @@ interface IERC5269 { /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify. /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](/EIPS/eip-721) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior. /// @param extraData, a `bytes` for [EIP-5750](/EIPS/eip-5750) for future extensions. - /// @return eipStatus a bytes32 indicating the status of EIP the contract supports. + /// @return eipStatus, a `bytes32` indicating the status of EIP the contract supports. /// - For FINAL EIPs, it MUST return `keccak256("FINAL")`. /// - For non-FINAL EIPs, it SHOULD return `keccak256("DRAFT")`. /// During EIP procedure, EIP authors are allowed to specify their own @@ -92,20 +94,20 @@ interface IERC5269 { } ``` -See [`IERC5269.sol`](../assets/eip-5269/contracts/IERC5269.sol). +In the following description, `EIP_5269_STATUS` is set to be `keccak256("DRAFTv1")`. In addition to the behavior specified in the comments of `IERC5269`: 1. Any `minorEIPIdentifier=0` is reserved to be referring to the main behavior of the EIP being queried. 2. Authors of compliant EIP is RECOMMENDED to declare a list of `minorEIPIdentifier` for their optional interfaces, behaviors and value range for future extension. -3. When this EIP is FINAL, any compliant contract that is an `IERC5629` MUST return `keccak256("FINAL")` for the call of `supportEIP((any caller), 5269, 0, [])`. +3. When this EIP is FINAL, any compliant contract MUST return a `EIP_5269_STATUS` for the call of `supportEIP((any caller), 5269, 0, [])` -*Note*: at the current snapshot, `supportEIP((any caller), 5269, 0, [])` MUST return `keccak256("DRAFT")`. +*Note*: at the current snapshot, `supportEIP((any caller), 5269, 0, [])` MUST return `EIP_5269_STATUS`. -4. Any compliant contract SHOULD emit a `OnSupportEIP(true, address(0), 5269, 0, [])` -5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values. -6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`, it is RECOMMENDED to return `0`. -7. The `supportEIP` MUST be `view`, ie. it MUST NOT mutate any global state of EVM. +4. Any compliant contract SHOULD emit a `OnSupportEIP(address(0), 5269, 0, EIP_5269_STATUS, [])` event upon construction or upgrade. +5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values and when compliant contract changes whether the support an EIP or certain behavior for certain caller or all callers. +6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`. It is RECOMMENDED to return `0` in this case but other value of `eipStatus` is allowed. Caller MUST treat any returned value other than `keccak256("FINAL")` as non-final, and MUST treat 0 as strictly "not supported". +7. The `supportEIP` MUST be `view`, i.e. it MUST NOT mutate any global state of EVM. ## Rationale From e120ebad090f2a06008ea7913ca2f1920adfad37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toni=20Wahrst=C3=A4tter?= <51536394+nerolation@users.noreply.github.com> Date: Thu, 12 Jan 2023 11:24:21 +0100 Subject: [PATCH 167/274] Update design of eip-5564 (#6321) * update eip-5564 * Update eip-5564 * Update eip-5564.md * Update eip-5564.md --- EIPS/eip-5564.md | 329 ++++++++++++++------ assets/eip-5564/minimal_poc.ipynb | 491 ++++++++++++++++++++++++++++-- 2 files changed, 690 insertions(+), 130 deletions(-) diff --git a/EIPS/eip-5564.md b/EIPS/eip-5564.md index 03d8d51e31d75a..c66ebf67a36f83 100644 --- a/EIPS/eip-5564.md +++ b/EIPS/eip-5564.md @@ -1,163 +1,294 @@ --- eip: 5564 -title: Stealth Address Wallets -description: Stealth addresses for smart contract wallets -author: Anton Wahrstätter (@Nerolation) +title: Non-Interactive Stealth Address Generation +description: Stealth addresses for private transfers +author: Toni Wahrstätter (@nerolation), Matt Solomon (@mds1), Ben DiFrancesco (@apbendi), Vitalik Buterin discussions-to: https://ethereum-magicians.org/t/eip-5566-stealth-addresses-for-smart-contract-wallets/10614 status: Draft type: Standards Track category: ERC created: 2022-08-13 -requires: 165 --- ## Abstract -This specification defines a standardized way of creating stealth addresses for smart contract wallets. This EIP enables senders of transactions/transfers to generate private stealth addresses for their recipients that only the recipient can eventually unlock. + +This specification defines a standardized way of creating stealth addresses. This EIP enables senders of transactions/transfers to non-interactively generate private stealth addresses for their recipients that only the recipients can unlock. ## Motivation -The standardization of stealth address generation may unlock significant privacy potential within Smart Contract Wallets, allowing the recipient of a transfer to remain private when receiving an asset. A Stealth address is generated by the sender from a shared secret between sender and recipient and can only be unlocked by the recipient because only the recipient can compute the matching private key. -Observers have no possibility of linking the recipient's stealth address to the recipient's identity, leaving only the sender with that information. +The standardization of non-interactive stealth address generation holds the potential to greatly enhance the privacy capabilities of Ethereum by enabling the recipient of a transfer to remain anonymous when receiving an asset. This is achieved through the generation of a stealth address by the sender, using a shared secret between the sender and recipient. Only the recipient is able to unlock the funds at the stealth address, as they are the only ones with access to the private key required for this purpose. As a result, observers are unable to link the recipient's stealth address to their identity, preserving the privacy of the recipient and leaving only the sender with this information. ## 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. -Every smart contract compliant with this EIP wallet MUST implement the [EIP-165](./eip-165.md) (`0x01ffc9a7`) interface. +The follow contracts are part of this specification: + +- `IERC5564Registry` stores the stealth public keys for users. This MUST be a singleton contract, with one instance per chain. + +- `IERC5565Generator` contracts are used to compute stealth addresses for a user based on a given curve. There can be many of these per chain, and for a given curve there SHOULD be one implementation per chain. Generator contracts are intended to primarily serve as reference implementations for off-chain libraries, as calling a method over HTTPS to generate a stealth address may compromise the user's privacy depending on who runs the node. + +- `IERC5564Messenger` emits events to announce when something is sent to a stealth address. This MUST be a singleton contract, with one instance per chain. + +The interface for each is specified as follows: + +### `IERC5564Registry` ```solidity -// SPDX-License-Identifier: CC0-1.0 -pragma solidity ^0.8.6; -... -interface IERC5564 { - - /// @notice Public Key coordinates of the wallet owner - /// @dev Is used by other wallets to generate stealth addresses - /// on behalf of the wallet owner. - bytes publicKey; - - - /// @notice Generates a stealth address that can be accessed only by the recipient. - /// @dev Function is executed locally by the sender on the recipient's wallet to - /// generate a stealthAddress and publishableData S. The Caller/Sender must select a secret - /// value s and compute the stealth address of the wallet owner and the matching public key S - /// to the selected secret s. - /// @param secret A secret value selected by the sender - function generateStealthAddress(uint256 secret) returns (bytes publishableData, address stealthAddress) +/// @notice Registry to map an address to its stealth key information. +interface IERC5564Registry { + /// @notice Returns the stealth public keys for the given `registrant` to compute a stealth + /// address accessible only to that `registrant` using the provided `generator` contract. + /// @dev MUST return zero if a registrant has not registered keys for the given generator. + function stealthKeys(address registrant, address generator) + external + view + returns (bytes memory spendingPubKey, bytes memory viewingPubKey); + + /// @notice Sets the caller's stealth public keys for the `generator` contract. + function registerKeys(address generator, bytes memory spendingPubKey, bytes memory viewingPubKey) + external; + + /// @notice Sets the `registrant`s stealth public keys for the `generator` contract using their + /// `signature`. + /// @dev MUST support both EOA signatures and EIP-1271 signatures. + function registerKeysOnBehalf( + address registrant, + address generator, + bytes memory signature, + bytes memory spendingPubKey, + bytes memory viewingPubKey + ) external; + + /// @dev Emitted when a registrant updates their registered stealth keys. + event StealthKeyChanged( + address indexed registrant, address indexed generator, bytes spendingPubKey, bytes viewingPubKey + ); +} +``` +### `IERC5564Generator` + +```solidity +/// @notice Interface for generating stealth addresses for keys from a given stealth address scheme. +/// @dev The Generator contract MUST have a method called `stealthKeys` that returns the recipient's +/// public keys as the correct types. The return types will vary for each generator, so a sample +/// is shown below. +interface IERC5564Generator { + /// @notice Given a `registrant`, returns all relevant data to compute a stealth address. + /// @dev MUST return all zeroes if the registrant has not registered keys for this generator. + /// @dev The returned `viewTag` MUST be the hash of the `sharedSecret`. THe hashing function used + /// is specified by the generator. + /// @dev `ephemeralPubKey` represents the ephemeral public key used by the sender. + /// @dev Intended to be used off-chain only to prevent exposing secrets on-chain. + /// @dev Consider running this against a local node, or using an off-chain library with the same + /// logic, instead of via an `eth_call` to a public RPC provider to avoid leaking secrets. + function generateStealthAddress(address registrant) + external + view + returns ( + address stealthAddress, + bytes memory ephemeralPubKey, + bytes memory sharedSecret, + bytes32 viewTag + ); + + /// @notice Returns the stealth public keys for the given `registrant`, in the types that best + /// represent the curve. + /// @dev The below is an example for the secp256k1 curve. + function stealthKeys(address registrant) + external + view + returns ( + uint256 spendingPubKeyX, + uint256 spendingPubKeyY, + uint256 viewingPubKeyX, + uint256 viewingPubKeyY + ); } +``` + +### `IERC5564Messenger` -interface PubStealthInfoContract { - - /// @noticeImmutable contract that broadcasts an - /// event with the address of the stealthRecipient and - /// publishableData S for every privateTransfer. - /// @dev Emits event with private transfer information S and the recipient's address. - /// S is generated by the sender and represents the public key to the secret s. - /// The sender broadcasts S for every private transfer. Users can use S to check if they were - /// the recipients of a respective transfer by comparing it to stealthRecipient. - /// @param stealthRecipient The address to send the funds to - /// @param publishableData The public key to the sender's secret - event PrivateTransfer(address indexed stealthRecipient, bytes publishableData) +```solidity +/// @notice Interface for announcing that something was sent to a stealth address. +interface IERC5564Messenger { + /// @dev Emitted when sending something to a stealth address. + /// @dev See `announce` for documentation on the parameters. + event Announcement( + bytes ephemeralPubKey, bytes32 indexed stealthRecipientAndViewTag, bytes32 metadata + ); + + /// @dev Called by integrators to emit an `Announcement` event. + /// @dev `ephemeralPubKey` represents the ephemeral public key used by the sender. + /// @dev `stealthRecipientAndViewTag` contains the stealth address (20 bytes) and the view tag (12 + /// bytes). + /// @dev `metadata` is an arbitrary field that the sender can use however they like, but the below + /// guidelines are recommended: + /// - When sending ERC-20 tokens, the metadata SHOULD include the token address as the first 20 + /// bytes, and the amount being sent as the following 32 bytes. + /// - When sending ERC-721 tokens, the metadata SHOULD include the token address as the first 20 + /// bytes, and the token ID being sent as the following 32 bytes. + function announce( + bytes memory ephemeralPubKey, + bytes32 stealthRecipientAndViewTag, + bytes32 metadata + ) external; } ``` -#### Stealth Address Generation (executed locally) +### Sample Generator Implementation ```solidity -function generateStealthAddress(uint256 secret) public view returns (bytes, address){ - // s*G = S - pubkeyFromSecret = ecMul(secret, G); - // s*P = q - sharedSecret = ecMul(secret, Publickey); - // hash(sharedSecret) - hashedSharedSecret = keccak256(sharedSecret); - // hash value to public key - pubkeyFromHashedSecret = ecMul(hashedSharedSecret, G); - // derive new public key - stealthPubkeyRecipient = ecAdd(Publickey, pubkeyFromHashedSecret); - // generate stealth address - stealthAddressRecipient = address(stealthPubkeyRecipient); - // return public key coordinates and stealthAddress - return (pubkeyFromSecret, stealthAddressRecipient); +/// @notice Sample IERC5564Generator implementation for the secp256k1 curve. +contract Secp256k1Generator is IERC5564Generator { + /// @notice Address of this chain's registry contract. + IERC5564Registry public constant REGISTRY = IERC5564Registry(address(0)); + + /// @notice Sample implementation for parsing stealth keys on the secp256k1 curve. + function stealthKeys(address registrant) + external + view + returns ( + uint256 spendingPubKeyX, + uint256 spendingPubKeyY, + uint256 viewingPubKeyX, + uint256 viewingPubKeyY + ) + { + // Fetch the raw spending and viewing keys from the registry. + (bytes memory spendingPubKey, bytes memory viewingPubKey) = + REGISTRY.stealthKeys(registrant, address(this)); + + // Parse the keys. + assembly { + spendingPubKeyX := mload(add(spendingPubKey, 0x20)) + spendingPubKeyY := mload(add(spendingPubKey, 0x40)) + viewingPubKeyX := mload(add(viewingPubKey, 0x20)) + viewingPubKeyY := mload(add(viewingPubKey, 0x40)) } + } + + /// @notice Sample implementation for generating stealth addresses for the secp256k1 curve. + function generateStealthAddress(address registrant, bytes memory ephemeralPrivKey) + external + view + returns ( + address stealthAddress, + bytes memory ephemeralPubKey, + bytes memory sharedSecret, + bytes32 viewTag + ) + { + // Get the ephemeral public key from the private key. + ephemeralPubKey = ecMul(ephemeralPrivKey, G); + + // Get user's parsed public keys. + ( + uint256 spendingPubKeyX, + uint256 spendingPubKeyY, + uint256 viewingPubKeyX, + uint256 viewingPubKeyY + ) = stealthKeys(registrant, address(this)); + + // Generate shared secret from sender's private key and recipient's viewing key. + sharedSecret = ecMul(ephemeralPrivKey, viewingPubKeyX, viewingPubKeyY); + bytes32 sharedSecretHash = keccak256(sharedSecret); + + // Generate view tag for enabling faster parsing for the recipient + viewTag = sharedSecretHash[0:12]; + + // Generate a point from the hash of the shared secret + bytes memory sharedSecretPoint = ecMul(sharedSecret, G); + + // Generate sender's public key from their ephemeral private key. + bytes memory stealthPubKey = ecAdd(spendingPubKeyX, spendingPubKeyY, sharedSecretPoint); + + // Compute stealth address from the stealth public key. + stealthAddress = pubkeyToAddress(stealthPubKey); + } ``` +Stealth addresses are computed using the algorithm below, assuming elliptic curves. +Other encryption schemes such as post-quantum encryption with Kyber may need to modify this approach. ---- - -#### For transfering: +- $G$ is the generator point of the curve. -Recipient Keypair --> $(p,P) | P = p * G$ +- Recipient has private keys $p_{view}$ and $p_{spend}$. +- Recipient publishes corresponding public keys $P_{view}$ and $P_{spend}$ in the `IERC5564Registry`. -##### Recipient: -```solidity -bytes public pubkeyRecipient; -``` -NOTE: `pubkeyRecipient` MUST be incorporated in the smart contract wallet contract as a public state variable. +- Sender generates random 32-byte entropy ephemeral private key $p_{ephemeral}$. ---- +- Sender passes the recipient address and $p_{ephemeral}$ to the `IERC5564Generator` contract's `generateStealthAddress` function. -##### Sender: +- This function performs the following computations: + - A shared secret $s$ is computed as $s = p_{ephemeral} \cdot P_{view}$. + - The secret is hashed $s_{h} = h(s)$. + - The view tag $v$ is extracted by taking the most significant 12 bytes $s_{h}[0:12]$, + - Multiplying the shared secret with the generator point $S = s \cdot G$. + - The recipient's stealth public key is computed as $P_{stealth} = P_{spend} + S$. + - The recipient's stealth address $a_{stealth}$ is computed as $\textrm{pubkeyToAddress}(P_{stealth})$. -Sender senderKeypair --> $(s,S) | S = s * G $ -```solidity -bytes pubkeyFromSecret = ecMul(s, G); -``` -NOTE: The parameter `s` represents a sender-generated secret and MUST NOT equal a user's private key. +Sending funds now works as follows: -$sharedSecret = s * P$ +- Sender uses the contract of their choice to send something to $a_{stealth}$, and provides $P_{ephemeral}$ and any other metadata to the send method. -```solidity -bytes sharedSecret = ecMul(s, pubkeyRecipient); -``` +- The contract calls `IERC5564Messenger.announce` with $a_{stealth}$, $v$, $P_{ephemeral}$, and any metadata. +To scan for funds, a recipient must retrieve all logs from the `IERC5564Messenger` contract. +They then check if they can compute the stealth address $P_{stealth}$ that was emitted as stealth address $a_{stealth}$ in the `Announcement`. If successful, the recipient can generate $p_{stealth}$, representing the private key that can eventually access $P_{stealth}$. +The parsing process can be presented as follows: +- Recipient has private keys $p_{view}$ and $p_{spend}$. -stealthAddress = $pubtoaddr(P + (G * keccak(sharedSecret)))$ -```solidity -bytes32 hashedSharedSecret = keccak256(sharedSecret); -bytes pubkeyFromHashedSecret = ecMul(hashedSharedSecret, G); -bytes stealthPubkeyRecipient = ecAdd(pubkeyRecipient, pubkeyFromHashedSecret); -address stealthAddressRecipient = address(stealthPubkeyRecipient); -``` -Transfer MUST emit a `PrivateTransfer` Event containing `S` and the `stealthAddressRecipient`. +- Recipient parses all Announcements $a_i$ performs the following operations: +- This function performs the following computations: + - Computing the shared secret $s$ is computed as $s = a_{i, P_{ephemeral}} \cdot p_{view}$. + - Hashing the shared secret, $s_{h} = h(s)$. + - Comparing the most significant 12 bytes of the resulting hash with the view tag emitted in the event and continue if they match. + - Multiplying the shared secret with the generator point $S = s \cdot G$. + - Compute stealth public key as $P_{stealth} = P_{spend} + S$. + - The recipient's address is computed as $a_{stealth} = \textrm{pubkeyToAddress}(P_{stealth})$. + - Compare $a_{stealth}$ with the stealth address logged the emitted `Announcement` event. ---- +### Parsing considerations +Usually, the recipient of a stealth address transaction has to perform the following operations to check weather he was the recipient of a certain transaction: -#### For receiving (executed locally): +- 2x ecMUL, -for all `PrivateTransfer` events do: +- 2x HASH, -if $pubtoaddr(P + (G * keccak(S * p)))$ == $stealthAddressRecipient$: -```solidity -bytes32 hashedSecret = keccak256(S); -bytes sharedSecret = ecMul(hashedSecret, p); -bytes pubkeyFromSharedSecret = ecMul(sharedSecret, G); -bytes stealthPubkeyRecipient = ecAdd(pubkeyRecipient, pubkeyFromSharedSecret); -address stealthAddressRecipient = address(stealthPubkeyRecipient); -store_key_locally(p + keccak(S * p)); -``` +- 1x ecADD, +The view tags approach is introduced to reduce the parsing time by around 6x. Users only need to perform 1x ecMUL and 1x HASH (skipping 1x ecMUL, 1x ecADD and 1x HASH) for every parsed announcement. The 12 bytes length was is based on the freely available space in the first log of the `Announcement` Event. With 12 bytes as `viewTag` the probability for users to skip the remaining computations after hashing the shared secret $h(s)$ can be determined as follows: $1/(256^{12})$. This means that users can almost certainly skip the above three operations for any announcements that to do not involve them. ## Rationale -This EIP emerged from the need of having privacy-preserving ways to transfer ownership without revealing the recipient's identity. Tokens can reveal sensitive private information about the owner. While users might want to prove the ownership of an NFT concert ticket, they might not want to reveal personal account-related information at the same time. The standardization of stealth address generation represents a significant effort for privacy. Privacy-preserving solutions require standards to gain adoption, therefore it is critical to focus on generalizable ways of implementing related solutions. -This extension standardizes the method to generate and look up stealth addresses. Users can send assets without having to interact with the recipient beforehand. Furthermore, users can verify if they have been the recipient of a transfer without requiring interactions with the chain. Stealth addresses allow only the recipients of token transfers to see that they were the recipients. +This EIP emerged from the need of having privacy-preserving ways to transfer ownership without revealing the recipient's identity. Tokens can reveal sensitive private information about the owner. While users might want to donate money to a specific organization/country but they might not want to reveal personal account-related information at the same time. The standardization of stealth address generation represents a significant effort for privacy: privacy-preserving solutions require standards to gain adoption, therefore it is critical to focus on generalizable ways of implementing related solutions. + +The stealth address extension standardizes a protocol for generating and locating stealth addresses, enabling the transfer of assets without the need for prior interaction with the recipient and allowing recipients to verify the receipt of a transfer without interacting with the blockchain. Importantly, stealth addresses allow the recipient of a token transfer to verify receipt while maintaining their privacy, as only the recipient is able to see that they have been the recipient of the transfer. + +The authors identify the trade-off between on- and off-chain efficiency: Although, including a Monero-like `view tags` mechanism helps recipients to parse announcements more quickly, it adds complexity to the announcement event. +The address of the recipient and the `viewTag` MUST be included in the announcement event, allowing users to quickly verify ownership without having to query the chain for positive account balances. ## Backwards Compatibility -No backward compatibility issues were found. + +This EIP is fully backward compatible. ## Reference Implementation -You can find an implementation of this standard in [gnosisSafeModule.sol](../assets/eip-5564/gnosisSafeModule.sol). + +You can find an implementation of this standard in TBD. ## Security Considerations -The funding of the stealth address wallet represents a known issue that might breach privacy. The wallet that funds the stealth address wallet MUST NOT have any physical connection to the stealth address owner in order to fully leverage the privacy improvements. + +The funding of the stealth address wallet represents a known issue that might breach privacy. The wallet that funds the stealth address MUST NOT have any physical connection to the stealth address owner in order to fully leverage the privacy improvements. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5564/minimal_poc.ipynb b/assets/eip-5564/minimal_poc.ipynb index db16b65696bba5..c7e589d14fc45b 100644 --- a/assets/eip-5564/minimal_poc.ipynb +++ b/assets/eip-5564/minimal_poc.ipynb @@ -7,10 +7,7 @@ "metadata": {}, "outputs": [], "source": [ - "# Note that this poc is based on the reply of Vitalik on EthResearch here:\n", - "# https://ethresear.ch/t/erc721-extension-for-zk-snarks/13237/13\n", - "\n", - "# The code is not part of the zk-SNARK implementation an will be moved." + "# PoC using scaning and spending keys" ] }, { @@ -26,6 +23,14 @@ "from eth_account import Account" ] }, + { + "cell_type": "markdown", + "id": "4e25cb04", + "metadata": {}, + "source": [ + "## Sender" + ] + }, { "cell_type": "markdown", "id": "22ca0bf7", @@ -60,6 +65,14 @@ "S" ] }, + { + "cell_type": "markdown", + "id": "c8240f67", + "metadata": {}, + "source": [ + "## Recipient" + ] + }, { "cell_type": "markdown", "id": "6895e603", @@ -77,8 +90,10 @@ { "data": { "text/plain": [ - "(89565891926547004231252920425935692360644145829622209833684329913297188986597,\n", - " 12158399299693830322967808612713398636155367887041628176798871954788371653930)" + "((89565891926547004231252920425935692360644145829622209833684329913297188986597,\n", + " 12158399299693830322967808612713398636155367887041628176798871954788371653930),\n", + " (112711660439710606056748659173929673102114977341539408544630613555209775888121,\n", + " 25583027980570883691656905877401976406448868254816295069919888960541586679410))" ] }, "execution_count": 4, @@ -89,9 +104,12 @@ "source": [ "# privkey: 0x0000000000000000000000000000000000000000000000000000000000000001\n", "# address: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf\n", - "p = int(0x0000000000000000000000000000000000000000000000000000000000000002) # private key\n", - "P = secp256k1.privtopub(p.to_bytes(32, \"big\")) # public key\n", - "P" + "p_scan = int(0x0000000000000000000000000000000000000000000000000000000000000002) # private key\n", + "p_spend = int(0x0000000000000000000000000000000000000000000000000000000000000003) # private key\n", + "\n", + "P_scan = secp256k1.privtopub(p_scan.to_bytes(32, \"big\")) # public key\n", + "P_spend = secp256k1.privtopub(p_spend.to_bytes(32, \"big\")) # public key\n", + "P_scan, P_spend" ] }, { @@ -99,20 +117,28 @@ "id": "174929d7", "metadata": {}, "source": [ - "$P + G*hash(Q)$" + "## Calculate Stealth Address: $P_{spend} + G*hash(Q)$" + ] + }, + { + "cell_type": "markdown", + "id": "8b39ed39", + "metadata": {}, + "source": [ + "$Q = S * p_{scan}$" ] }, { "cell_type": "code", "execution_count": 5, - "id": "5f5fbcf4", + "id": "63a022d7", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(34986710196311298046001056196012031484809340405385241652824827959002221322031,\n", - " 37835699564152814660978587905143437663484333033321007899868184134470225816011)" + "(65311808848028536848162101908966111079795231803322390815513763038079235257196,\n", + " 43767810034999830518515787564234053904327508763526333662117780420755425490082)" ] }, "execution_count": 5, @@ -121,13 +147,226 @@ } ], "source": [ - "Q = secp256k1.multiply(S, p)\n", - "assert Q == secp256k1.multiply(P, s)\n", - "Q_hex = sha3.keccak_256(Q[0].to_bytes(32, \"big\")+Q[1].to_bytes(32, \"big\")).hexdigest() # note, toStr conversion\n", + "Q = secp256k1.multiply(P_scan, s)\n", + "Q" + ] + }, + { + "cell_type": "markdown", + "id": "d79c69fc", + "metadata": {}, + "source": [ + "$Q = S * p_{scan} = P_{scan} * s$" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "5f5fbcf4", + "metadata": {}, + "outputs": [], + "source": [ + "assert Q == secp256k1.multiply(S, p_scan)" + ] + }, + { + "cell_type": "markdown", + "id": "0d5803ff", + "metadata": {}, + "source": [ + "$h(Q)$" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f1b38cb0", + "metadata": {}, + "outputs": [], + "source": [ + "Q_hex = sha3.keccak_256(Q[0].to_bytes(32, \"big\") \n", + " + Q[1].to_bytes(32, \"big\")\n", + " ).hexdigest()\n", + "Q_hased = bytearray.fromhex(Q_hex)" + ] + }, + { + "cell_type": "markdown", + "id": "a0647821", + "metadata": {}, + "source": [ + "$ stA = h(Q) * G + P_{spend}$" + ] + }, + { + "cell_type": "markdown", + "id": "865e7f72", + "metadata": {}, + "source": [ + "#### Sender sends funds to..." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "d9dd755f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xfed69df0a27f1dae0d7430ead82aaedfad6332bb'" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "stP = secp256k1.add(P_spend, secp256k1.privtopub(Q_hased))\n", + "stA = \"0x\"+ sha3.keccak_256(stP[0].to_bytes(32, \"big\")\n", + " +stP[1].to_bytes(32, \"big\")\n", + " ).hexdigest()[-40:]\n", + "stA" + ] + }, + { + "cell_type": "markdown", + "id": "38e69080", + "metadata": {}, + "source": [ + "#### Sender broadcasts" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "cdf57fef", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "((22246744184454969143801186698733154500632648736073949898323976612504587645286,\n", + " 110772761940586493986212935445517909380300793379795289150161960681985511655321),\n", + " '0xfed69df0a27f1dae0d7430ead82aaedfad6332bb')" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "S, stA" + ] + }, + { + "cell_type": "markdown", + "id": "588ccc7c", + "metadata": {}, + "source": [ + "## Parse received funds" + ] + }, + { + "cell_type": "markdown", + "id": "462f8c8d", + "metadata": {}, + "source": [ + "* Note that $p_{scan}$ and $P_{spend}$ can be shared with a trusted party\n", + "* There may be many S to be parsed" + ] + }, + { + "cell_type": "markdown", + "id": "8ba2a295", + "metadata": {}, + "source": [ + "$h(p_{scan}*S)*G + P_{spend} => toAddress$" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "50b63208", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xfed69df0a27f1dae0d7430ead82aaedfad6332bb'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Q = secp256k1.multiply(S, p_scan)\n", + "Q_hex = sha3.keccak_256(Q[0].to_bytes(32, \"big\")+Q[1].to_bytes(32, \"big\")).hexdigest()\n", "Q_hased = bytearray.fromhex(Q_hex)\n", "\n", - "# Sender sends to ...\n", - "secp256k1.add(P, secp256k1.privtopub(Q_hased))" + "P_stealth = secp256k1.add(P_spend, secp256k1.privtopub(Q_hased))\n", + "P_stealthAddress = \"0x\"+ sha3.keccak_256(stP[0].to_bytes(32, \"big\")\n", + " + stP[1].to_bytes(32, \"big\")\n", + " ).hexdigest()[-40:]\n", + "P_stealthAddress" + ] + }, + { + "cell_type": "markdown", + "id": "8055d075", + "metadata": {}, + "source": [ + "logged stealth address $stA$ equals the derived stealth address $P_stealthAddress$" + ] + }, + { + "cell_type": "markdown", + "id": "26758ea5", + "metadata": {}, + "source": [ + "$stA==stA_d$" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3faed6a3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P_stealthAddress == stA" + ] + }, + { + "cell_type": "markdown", + "id": "050e346c", + "metadata": {}, + "source": [ + "## Derive private key" + ] + }, + { + "cell_type": "markdown", + "id": "44801516", + "metadata": {}, + "source": [ + "#### Only the recipient has access to $p_{spend}$" ] }, { @@ -135,30 +374,60 @@ "id": "7673e439", "metadata": {}, "source": [ - "$p+hash(Q)$" + "$p_{stealth}=p_{spend}+hash(Q)$" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 12, "id": "4013b57e", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "(34986710196311298046001056196012031484809340405385241652824827959002221322031,\n", - " 37835699564152814660978587905143437663484333033321007899868184134470225816011)" + "39153944482575822531387237249775711740128993925789544779866399859639729033274" ] }, - "execution_count": 6, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Q = secp256k1.multiply(S, p_scan)\n", + "Q_hex = sha3.keccak_256(Q[0].to_bytes(32, \"big\")+Q[1].to_bytes(32, \"big\")).hexdigest()\n", + "p_stealth = p_spend + int(Q_hex, 16)\n", + "p_stealth" + ] + }, + { + "cell_type": "markdown", + "id": "dc31c1aa", + "metadata": {}, + "source": [ + "$P_{stealth} = p_{stealth}*G$" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "09b5ccc2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(67663851387124608323744162645277269585638670865381831245083336172545348387042,\n", + " 80449904826544093817252981338261706033086352950841917067356875711772573870404)" + ] + }, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "p_stealth = p + int(Q_hex, 16)\n", - "\n", "# Recipient has private key to ...\n", "P_stealth = secp256k1.privtopub(p_stealth.to_bytes(32, \"big\"))\n", "P_stealth" @@ -166,17 +435,41 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 14, + "id": "a3ead30e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xfed69df0a27f1dae0d7430ead82aaedfad6332bb'" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P_stealthAddress_d = \"0x\"+ sha3.keccak_256(P_stealth[0].to_bytes(32, \"big\")\n", + " + P_stealth[1].to_bytes(32, \"big\")\n", + " ).hexdigest()[-40:]\n", + "P_stealthAddress_d" + ] + }, + { + "cell_type": "code", + "execution_count": 15, "id": "2712c07b", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'0xa423e468c7987026D1dC797425a870C5e704E75d'" + "'0xfEd69Df0a27F1daE0D7430EAd82aaEdfAD6332bb'" ] }, - "execution_count": 7, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -184,13 +477,149 @@ "source": [ "Account.from_key((p_stealth).to_bytes(32, \"big\")).address" ] + }, + { + "cell_type": "markdown", + "id": "74f0325e", + "metadata": {}, + "source": [ + "## Additionally add view tags" + ] + }, + { + "cell_type": "markdown", + "id": "ac45bb87", + "metadata": {}, + "source": [ + "In addition to S and stA, the sender also broadcasts the first byte of h(Q)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "9645b880", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "86" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Q_hased[0]" + ] + }, + { + "cell_type": "markdown", + "id": "8788f2f5", + "metadata": {}, + "source": [ + "The recipient can do the the same a before without one EC Multiplication, one EC Addition and on Public Key to Address Conversion in order to check being a potential recipient." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "bb9f5852", + "metadata": {}, + "outputs": [], + "source": [ + "Q_derived = secp256k1.multiply(S, p_scan)\n", + "Q_hex_derived = sha3.keccak_256(Q_derived[0].to_bytes(32, \"big\")\n", + " +Q_derived[1].to_bytes(32, \"big\")\n", + " ).hexdigest()\n", + "Q_hashed_derived = bytearray.fromhex(Q_hex_derived)" + ] + }, + { + "cell_type": "markdown", + "id": "f7dc4624", + "metadata": {}, + "source": [ + "Check view tag" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "953bf07d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "run = Q_hased[0] == Q_hashed_derived[0] \n", + "run" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "e11ec134", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'0xfed69df0a27f1dae0d7430ead82aaedfad6332bb'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "if run:\n", + " P_stealth = secp256k1.add(P_spend, secp256k1.privtopub(Q_hased))\n", + " P_stealthAddress = \"0x\"+ sha3.keccak_256(stP[0].to_bytes(32, \"big\")\n", + " + stP[1].to_bytes(32, \"big\")\n", + " ).hexdigest()[-40:]\n", + "P_stealthAddress" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "bd06ffc5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "P_stealthAddress==stA" + ] } ], "metadata": { "kernelspec": { - "display_name": "stealth2", + "display_name": "hackathon", "language": "python", - "name": "stealth2" + "name": "hackathon" }, "language_info": { "codemirror_mode": { @@ -202,7 +631,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.4" + "version": "3.10.8" } }, "nbformat": 4, From 3f6dec1b006ce6d868ecf81cab0de21377de54d4 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Thu, 12 Jan 2023 11:28:55 -0800 Subject: [PATCH 168/274] Update EIP-5269 (#6320) * Move to REview * Update eip-5269.md * Update * Fix --- EIPS/eip-5269.md | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index 9896ac0d03a2b6..161cb9ff7a872e 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -14,50 +14,47 @@ requires: 5750 ## Abstract An interface for better identification and detection of EIP/ERC by numbers. +It designates a field in which it's called `majorEIPIdentifier` which is normally known or referred to as "EIP number". For example, `ERC-721` aka [EIP-721](./eip-721.md) has a `majorEIPIdentifier = 721`. This EIP has a `majorEIPIdentifier = 5269`. -It designate a field in which it's called `majorEIPIdentifier` which is normally known or referred to as "EIP number". For example, `ERC-721` aka [EIP-721](./eip-721.md) has a `majorEIPIdentifier = 721`. This EIP has a `majorEIPIdentifier = 5269`. - -Calling it as "majorEIPIdentifier" instead of "EIP number" makes it future proof: anticipating there is a possibility where future EIP is not numbered or if we want to -incorporate other type of standards. +Calling it a `majorEIPIdentifier` instead of `EIPNumber` makes it future-proof: anticipating there is a possibility where future EIP is not numbered or if we want to incorporate other types of standards. It also proposes a new concept of `minorEIPIdentifier` which is left for authors of individual EIP to define. For example, EIP-721's author may define `ERC721Metadata` interface as `minorEIPIdentifier= keccak256("ERC721Metadata")`. -It also proposes an event to allow contract to optionally declare EIPs they support. +It also proposes an event to allow smart contracts to optionally declare the EIPs they support. ## Motivation -This EIP is created as a supplement to and competing standard for against [EIP-165](./eip-165.md). +This EIP is created as a competing standard for [EIP-165](./eip-165.md). Here are the major differences between this EIP and [EIP-165](./eip-165.md). -1. [EIP-165](./eip-165.md) uses hash of method signature which basically declares the existence of method or list of methods, -therefore it requires a method to *exist* in the first place. In some case, some ERCs interface are not represented in the way -of method signature, such as some EIPs related to data format and signature schemes. -2. [EIP-165](./eip-165.md) doesn't provide query ability based on caller. This EIP respond `true` or `false` based on caller. - +1. [EIP-165](./eip-165.md) uses the hash of a method's signature which declares the existence of one method or multiple methods, +therefore it requires at least one method to *exist* in the first place. In some cases, some EIP/ERCs interface does not have a method, such as some EIPs related to data format and signature schemes or the "Soul-Bound-ness" aka SBT which could just revert a transfer call without needing any specific method. +1. [EIP-165](./eip-165.md) doesn't provide query ability based on the caller. +The compliant contract of this EIP will respond to whether it supports certain EIP *based on* a given caller. Here is the motivation for this EIP given EIP-165 already exists: 1. Using EIP/ERC numbers improves human readability as well as make it easier to work with named contract such as ENS. -2. Instead of using an EIP-165 identifier, we have seen an increasing interest for using EIP/ERC numbers as the way to identify or specify a EIP/ERCs. For example +2. Instead of using an EIP-165 identifier, we have seen an increasing interest to use EIP/ERC numbers as the way to identify or specify an EIP/ERC. For example - [EIP-5267](./eip-5267.md) specifies `extensions` to be a list of EIP numbers. -- [EIP-600](./eip-600.md), [EIP-601](./eip-601.md) specifies EIP number in `m / purpose' / subpurpose' / EIP' / wallet'` +- [EIP-600](./eip-600.md), and [EIP-601](./eip-601.md) specify an `EIP` number in the `m / purpose' / subpurpose' / EIP' / wallet'` path. - [EIP-5568](./eip-5568.md) specifies `The instruction_id of an instruction defined by an EIP MUST be its EIP number unless there are exceptional circumstances (be reasonable)` - [EIP-6120](./eip-6120.md) specifies `struct Token { uint eip; ..., }` where `uint eip` is an EIP number to identify EIPs. - `EIP-867`(Stagnant) proposes to create `erpId: A string identifier for this ERP (likely the associated EIP number, e.g. “EIP-1234”).` -3. Having a ERC/EIP number detection interface reduces the need for a lookup table in smart contract to -convert a function method or whole interface in any EIP/ERC in the bytes4 EIP-165 identifier into its respective EIP number and massively simplify the way to specify EIP for behavior expansion. +3. Having an ERC/EIP number detection interface reduces the need for a lookup table in smart contract to +convert a function method or whole interface in any EIP/ERC in the bytes4 EIP-165 identifier into its respective EIP number and massively simplifies the way to specify EIP for behavior expansion. -4. We also recognize a smart contract might have different behavior given different caller. Most notable use case is that a common practice when using Transparent Upgradable Pattern is to give Admin and Non-Admin caller different treatment when calling a Proxy. +4. We also recognize a smart contract might have different behavior given different caller accounts. One of the most notable use cases is that when using Transparent Upgradable Pattern, a proxy contract gives an Admin account and Non-Admin account different treatment when they call. ## Specification -In the following description we use EIP and ERC inter-exchangebly. This was because while most of the time the description applies to ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of queried. +In the following description, we use EIP and ERC inter-exchangeably. This was because while most of the time the description applies to an ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of a query. 1. Any compliant smart contract MUST implement the following interface @@ -99,33 +96,38 @@ In the following description, `EIP_5269_STATUS` is set to be `keccak256("DRAFTv1 In addition to the behavior specified in the comments of `IERC5269`: 1. Any `minorEIPIdentifier=0` is reserved to be referring to the main behavior of the EIP being queried. -2. Authors of compliant EIP is RECOMMENDED to declare a list of `minorEIPIdentifier` for their optional interfaces, behaviors and value range for future extension. -3. When this EIP is FINAL, any compliant contract MUST return a `EIP_5269_STATUS` for the call of `supportEIP((any caller), 5269, 0, [])` +2. The Author of compliant EIP is RECOMMENDED to declare a list of `minorEIPIdentifier` for their optional interfaces, behaviors and value range for future extension. +3. When this EIP is FINAL, any compliant contract MUST return an `EIP_5269_STATUS` for the call of `supportEIP((any caller), 5269, 0, [])` -*Note*: at the current snapshot, `supportEIP((any caller), 5269, 0, [])` MUST return `EIP_5269_STATUS`. +*Note*: at the current snapshot, the `supportEIP((any caller), 5269, 0, [])` MUST return `EIP_5269_STATUS`. -4. Any compliant contract SHOULD emit a `OnSupportEIP(address(0), 5269, 0, EIP_5269_STATUS, [])` event upon construction or upgrade. -5. Any compliant contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting event of `OnSupportEIP` with relevant values and when compliant contract changes whether the support an EIP or certain behavior for certain caller or all callers. -6. For any `EIP-XXX` that is NOT in `Final` status, when querying `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`. It is RECOMMENDED to return `0` in this case but other value of `eipStatus` is allowed. Caller MUST treat any returned value other than `keccak256("FINAL")` as non-final, and MUST treat 0 as strictly "not supported". -7. The `supportEIP` MUST be `view`, i.e. it MUST NOT mutate any global state of EVM. +4. Any complying contract SHOULD emit an `OnSupportEIP(address(0), 5269, 0, EIP_5269_STATUS, [])` event upon construction or upgrade. +5. Any complying contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting an event of `OnSupportEIP` with relevant values and when the compliant contract changes whether the support an EIP or certain behavior for a certain caller or all callers. +6. For any `EIP-XXX` that is NOT in `Final` status, when querying the `supportEIP((any caller), xxx, (any minor identifier), [])`, it MUST NOT return `keccak256("FINAL")`. It is RECOMMENDED to return `0` in this case but other values of `eipStatus` is allowed. Caller MUST treat any returned value other than `keccak256("FINAL")` as non-final, and MUST treat 0 as strictly "not supported". +7. The function `supportEIP` MUST be mutability `view`, i.e. it MUST NOT mutate any global state of EVM. ## Rationale -1. When data type `uint256 majorEIPIdentifier`, there are other alternative options such as (1) use a hashed version of EIP number, or (2) use raw number, (3) use EIP-165 identifier. The pros for (1) is it automatically supports any evolvement of future EIP numbering/naming convention. But the cons is it's not backward readable: seeing a `hash(EIP-number)` one usually can't easily guess what EIP number is. We choose the (2) in the rationale laid out in motivation. -2. We have a `bytes32 minorEIPIdentifier`, alternatively it could be (1) a number, forcing all EIP author to define its numbering for sub-behaviors so we go with a `bytes32` and asking EIP author to use a hash for a string name for their sub-behaviors which they are already doing by coming up with interface name or method name in their specification. +1. When data type `uint256 majorEIPIdentifier`, there are other alternative options such as: -3. Alternatively it's possible we add extra data as return value or an array of all EIP being supported but we are unsure how much value this complexity brings. +- (1) using a hashed version of the EIP number, +- (2) use a raw number, or +- (3) use an EIP-165 identifier. -4. Compared to [EIP-165](./eip-165.md), we also add an addition input of `address caller`, given the increasing popularity of proxy patterns such as those enabled by [EIP-1967](./eip-1967.md). One may ask: why not simply use `msg.sender`? This is because -we want to allow query them without transaction or a proxy contract to query if whether interface ERC-`number` will be available to -that particular sender. +The pros for (1) are that it automatically supports any evolvement of future EIP numbering/naming conventions. +But the cons are it's not backward readable: seeing a `hash(EIP-number)` one usually can't easily guess what their EIP number is. -5. We reserve the input `majorEIPIdentifier` greater than or equals 2^32 in case we need to support other collection of standards which are not ERC/EIP. +We choose the (2) in the rationale laid out in motivation. -## Test Cases +2. We have a `bytes32 minorEIPIdentifier` in our design decision. Alternatively, it could be (1) a number, forcing all EIP authors to define its numbering for sub-behaviors so we go with a `bytes32` and ask the EIP authors to use a hash for a string name for their sub-behaviors which they are already doing by coming up with interface name or method name in their specification. + +3. Alternatively, it's possible we add extra data as a return value or an array of all EIP being supported but we are unsure how much value this complexity brings and whether the extra overhead is justified. -Here is an example of test cases. -See Reference Implementation section for a reference implementation being tested. +4. Compared to [EIP-165](./eip-165.md), we also add an additional input of `address caller`, given the increasing popularity of proxy patterns such as those enabled by [EIP-1967](./eip-1967.md). One may ask: why not simply use `msg.sender`? This is because we want to allow query them without transaction or a proxy contract to query whether interface ERC-`number` will be available to that particular sender. + +1. We reserve the input `majorEIPIdentifier` greater than or equals `2^32` in case we need to support other collections of standards which is not an ERC/EIP. + +## Test Cases ```typescript @@ -189,7 +191,6 @@ See [`TestERC5269.ts`](../assets/eip-5269/test/TestERC5269.ts). Here is a reference implementation for this EIP: ```solidity - contract ERC5269 is IERC5269 { bytes32 constant public EIP_STATUS = keccak256("DRAFTv1"); constructor () { From 250420822fb0a6bf96e2703e79325896fa697827 Mon Sep 17 00:00:00 2001 From: Dhruv Malik Date: Sun, 15 Jan 2023 02:51:16 +0100 Subject: [PATCH 169/274] Add EIP-5851: On-Chain Verifiable Credentials (#5851) * WIP: defining ERC standard for ZK based KYC verifier. * modified: EIPS/eip-draft-ZKID-standard.md * WIp: description of the functions * WIP: metadata description * removing metadata to local docs, adding examples * WIp: adapting the certifier contract and interface * WIP: code refactor and adding examples * spaces and author name * WIP: removing eipv errors * contd * minor issues * removing identation issues * resolving another round of markdown and EIPV issues * last one hopefully !!! * Update spaces in-lining Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * link and image format Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * defining definition of ZK term Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Refactoring the description Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * removing redundant description of motivation points Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * refactor description Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update description points Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * explain the efficiency aspect * title description Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * addingdefinitions to be described * reference changes * did all the spec changes , along with renaming asset folder * minor refactor * minor corrections of formula * broken links * linting issues * adding new diagram * wip: define latest workflow diagram * leaf -> proof in setting information * feat: reviewed with all the intial corrections and diagram description * corrections pointed out by @Pandapip1 * never mind * removing lint issues * refactoring the diagram along with description of spec * WIP: rectifying the code contracts. * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * WIP: commit changes by the reviewer * Update EIPS/eip-5851.md Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * WIp: other description errors. * fix lint * fixing explanation * adding Verifiable description changes * contd * WIP: rewriting the standard (currently not completed). * changing the metadata details * WIp: defining the example of VC. * modified: EIPS/eip-5851.md * Update eip-5851.md * Update eip-5851.md * refactoring the description along with adding the new diagram. - WIP: @yuliu-debond to define the workflow regarding how the third party will integrate the standard. * update EIP examples and their description * update * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update IERC5851.sol * Update IERC5851.sol * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update IERC5851.sol * Update verification_modifier.sol * Update IERC5851.sol * Update eip-5851.md * Update IERC5851.sol * Update SBT_certification.sol * Create EIP-5851Verifier * Rename EIP-5851Verifier to EIP-5851Verifier.sol * Rename SBT_certification.sol to ERC5851Issuer.sol * Rename EIP-5851Verifier.sol to ERC5851Verifier.sol * Delete verification_modifier.sol * Update test.sol * Update ERC5851Verifier.sol * Update ERC5851Issuer.sol * Update ERC5851Issuer.sol * Update IERC5851.sol * Update ERC5851Issuer.sol * Create ERC5851Verifier.sol * Update test.sol * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Update eip-5851.md * Delete architecture-diagram-5851.png * modified: EIPS/eip-5851.md - Rectifying linting issues like spacing between the headings and spellings * adding suggestions from 5851. modified: EIPS/eip-5851.md modified: assets/eip-3475/ERC3475.sol modified: assets/eip-5851/contracts/interfaces/IERC5851.sol - l#3: minor refactoring - shifting the definitions to the specification section. - rewriting the description of claim structures. - black-ticks for marking code. - l#61: refactoring the description of metadata in the nature of exposure to given - changing name of metadata type to kind for standardised names. - refactoting the JSON description along with sentences description about the example requirement structure. - l#95: refactoring the definition. - defining the logic operations supported by the claim structure along with their ASCII code representation. - defining the abstract more comprehensively. - l#137: completing the description about the function logic. * Update definition ZKP * Update eip-5851.md * update json example format eip-5851.md * Update eip-5851.md * minr lint * Updated titile * Update eip-5851.md * resolving minor functions * improve clarity of 'value information' * Update eip-5851.md * add a blank line before/after fenced code blocks * Delete eip-5851.md * Delete ERC5851Issuer.sol * Delete ERC3475.sol * Create eip-5851.md * Create ERC5851Issuer.sol * Create ERC3475.sol * Update ERC3475.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Shebin John Co-authored-by: yuliu-debond <79855548+yuliu-debond@users.noreply.github.com> Co-authored-by: jooeys --- EIPS/eip-5851.md | 265 ++++++++++++++++++ assets/eip-5851/contracts/ERC5851Issuer.sol | 46 +++ assets/eip-5851/contracts/ERC5851Verifier.sol | 17 ++ .../contracts/interfaces/IERC5851.sol | 130 +++++++++ assets/eip-5851/contracts/test.sol | 17 ++ assets/eip-5851/script/offchainOperations.js | 69 +++++ 6 files changed, 544 insertions(+) create mode 100644 EIPS/eip-5851.md create mode 100644 assets/eip-5851/contracts/ERC5851Issuer.sol create mode 100644 assets/eip-5851/contracts/ERC5851Verifier.sol create mode 100644 assets/eip-5851/contracts/interfaces/IERC5851.sol create mode 100644 assets/eip-5851/contracts/test.sol create mode 100644 assets/eip-5851/script/offchainOperations.js diff --git a/EIPS/eip-5851.md b/EIPS/eip-5851.md new file mode 100644 index 00000000000000..a886e1442f7203 --- /dev/null +++ b/EIPS/eip-5851.md @@ -0,0 +1,265 @@ +--- +eip: 5851 +title: On-Chain Verifiable Credentials +description: Interface for contracts that manage verifiable claims and identifiers as Soulbound tokens. +author: Yu Liu (@yuliu-debond), Junyi Zhong (@Jooeys) +discussions-to: https://ethereum-magicians.org/t/eip-5815-kyc-certification-issuer-and-verifier-standard/11513 +status: Draft +type: Standards Track +category: ERC +created: 2022-10-18 +requires: 721, 1155, 1167, 1967, 3475 +--- +## Abstract + +This proposal introduces a method of certifying that a particular address meets a claim, and a method of verifying those certifications using on-chain metadata. Claims are assertions or statements made about a subject having certain properties that may be met conditions (for example: `age >= 18`), and are certified by issuers using a Soundbound Token (SBT). + +## Motivation + +On-chain issuance of verifiable attestations are essential for use-case like: + +- Avoiding Sybil attacks with one person one vote +- Participation in certain events with credentials +- Compliance to government financial regulations etc. + +We are proposing a standard claims structure for Decentralized Identity (DID) issuers and verifier entities to create smart contracts in order to provide on-chain commitment of the off-chain verification process, and once the given address is associated with the given attestation of the identity verification off-chain, the issuers can then onboard other verifiers (i.e. governance, financial institution, non-profit organization, web3 related cooperation) to define the condition of the ownership of the user in order to reduce the technical barriers and overhead of current implementations. + +The motivation behind this proposal is to create a standard for verifier and issuer smart contracts to communicate with each other in a more efficient way. This will reduce the cost of KYC processes, and provide the possibility for on-chain KYC checks. By creating a standard for communication between verifiers and issuers, it will create an ecosystem in which users can be sure their data is secure and private. This will ultimately lead to more efficient KYC processes and help create a more trustful environment for users. It will also help to ensure that all verifier and issuer smart contracts are up-to-date with the most recent KYC regulations. + +## Specification + +The keywords "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. + +### Definitions + +- Zero-Knowledge Proof (ZKP): a cryptographic device that can convince a verifier that an assertion is correct without revealing all of the inputs to the assertion. + +- Soulbound Token (SBT): A non-fungible and non-transferrable token that is used for defining the identity of the users. + +- SBT Certificate: An SBT that represents the ownership of ID signatures corresponding to the claims defined in `function standardClaim()`. + +- Verifiable Credential (VC): A collection of claims made by an issuer. These are temper evident credentials that allow the holders to prove that they posses certain characteristics (for example, passport verification, constraints like value of tokens in your wallet, etc) as demanded by the verifier entity. + +- Claim: An assertion that the DID Holder must fulfill to be verified. + +- Holder: The entity that stores the claim, such as a digital identity provider or a DID registry. The holder is responsible for validating the claim and providing verifiable evidence of the claim. + +- Claimer: The party making a claim, such as in an identity verification process. + +- Issuer: The entity that creates a verifiable credential from claims about one or more subjects to a holder. Example issuers include governments, corporations, non-profit organizations, trade associations, and individuals. + +- Verifier: An entity that validates data provided by an issuer of verifiable credentials, determining its accuracy, origin, currency and trustworthiness. + + + +### Metadata Standard + +Claims MUST be exposed in the following structures: + +#### 1. Metadata information + +Each claim requirement MUST be exposed using the following structure: + +```solidity + /** Metadata + * + * @param title defines the name of the claim field + * @param _type is the type of the data (bool,string,address,bytes,..) + * @param description additional information about claim details. + */ + struct Metadata { + string title; + string _type; + string description; + } +``` + +#### 2. Values Information + +This following structure will be used to define the actual claim information, based on the description of the `Metadata` structure, the structure is the same as `Values` structure of [EIP-3475](./eip-3475.md). + +```solidity + struct Values{ + string stringValue; + uint uintValue; + address addressValue; + bool boolValue; + } +``` + +#### 3. Claim structure + +Claims (eg. `age >= 18`, jurisdiction in allowlist, etc.) are represented by one or many instances of the `Claim` structure below: + +```solidity + /** Claims + * + * Claims structure consist of the conditions and value that holder claims to associate and verifier has to validate them. + * @notice the below given parameters are for reference purposes only, developers can optimize the fields that are needed to be represented on-chain by using schemes like TLV, encoding into base64 etc. + * @dev structure that defines the parameters for specific claims of the SBT certificate + * @notice this structure is used for the verification process, it contains the metadata, logic and expectation + * @notice logic can represent either the enum format for defining the different operations, or they can be logic operators (stored in form of ASCII figure based on unicode standard). like e.g: +("⊄" = U+2284, "⊂" = U+2282, "<" = U+003C , "<=" = U + 2265,"==" = U + 003D, "!="U + 2260, ">=" = U + 2265,">" = U + 2262). + */ + struct Claim { + Metadata metadata; + string logic; + Values expectation; + + } +``` + +description of some logic functions that can be used are as follows: + +| Symbol | Description | +|--------|--------------| +| ⊄ | does not belong to the set of values (or range) defined by the corresponding `Values` | +| ⊂ | condition that the parameter belongs to one of values defined by the `Values` | +| < | condition that the parameter is greater than value defined by the `Values` | +| == | condition that the parameter is strictly equal to the value defined by the `Values` structure | + +#### Claim Example + +```json +{ + "title":"age", + "type":"unit", + "description":"age of the person based on the birth date on the legal document", + "logic":">=", + "value":"18" +} +``` + +Defines the condition encoded for the index 1 (i.e the holder must be equal or more than 18 years old). + +### Interface specification + +#### Verifier + +```solidity + + /// @notice getter function to validate if the address `claimer` is the holder of the claim defined by the tokenId `SBTID` + /// @dev it MUST be defining the conditional operator (logic explained below) to allow the application to convert it into code logic + /// @dev logic given here MUST be the conditiaonl operator, MUST be one of ("⊄", "⊂", "<", "<=", "==", "!=", ">=", ">") + /// @param claimer is the EOA address that wants to validate the SBT issued to it by the issuer. + /// @param SBTID is the Id of the SBT that user is the claimer. + /// @return true if the assertion is valid, else false + /** + example ifVerified(0xfoo, 1) => true will mean that 0xfoo is the holder of the SBT identity token defined by tokenId of the given collection. + */ + function ifVerified(address claimer, uint256 SBTID) external view returns (bool); +``` + +#### Issuer + +```solidity + + /// @notice getter function to fetch the on-chain identification logic for the given identity holder. + /// @dev it MUST not be defined for address(0). + /// @param SBTID is the Id of the SBT that the user is the claimer. + /// @return the struct array of all the descriptions of condition metadata that is defined by the administrator for the given KYC provider. + /** + ex: standardClaim(1) --> { + { "title":"age", + "type": "uint", + "description": "age of the person based on the birth date on the legal document", + }, + "logic": ">=", + "value":"18" + } + Defines the condition encoded for the identity index 1, defining the identity condition that holder must be equal or more than 18 years old. + **/ + + function standardClaim(uint256 SBTID) external view returns (Claim[] memory); + + /// @notice function for setting the claim requirement logic (defined by Claims metadata) details for the given identity token defined by SBTID. + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which the admin wants to define the Claims. + /// @param `claims` is the struct array of all the descriptions of condition metadata that is defined by the administrator. check metadata section for more information. + /** + example: changeStandardClaim(1, { "title":"age", + "type": "uint", + "description": "age of the person based on the birth date on the legal document", + }, + "logic": ">=", + "value":"18" + }); + will correspond to the functionality that admin needs to adjust the standard claim for the identification SBT with tokenId = 1, based on the conditions described in the Claims array struct details. + **/ + + function changeStandardClaim(uint256 SBTID, Claim[] memory _claims) external returns (bool); + + /// @notice function which uses the ZKProof protocol to validate the identity based on the given + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which admin wants to define the Claims. + /// @param claimer is the address that needs to be proven as the owner of the SBT defined by the tokenID. + /** + example: certify(0xA....., 10) means that admin assigns the DID badge with id 10 to the address defined by the `0xA....` wallet. + */ + function certify(address claimer, uint256 SBTID) external returns (bool); + + /// @notice function which uses the ZKProof protocol to validate the identity based on the given + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which the admin wants to define the Claims. + /// @param claimer is the address that needs to be proven as the owner of the SBT defined by the tokenID. + /* eg: revoke(0xfoo,1): means that KYC admin revokes the SBT certificate number 1 for the address '0xfoo'. */ + function revoke(address certifying, uint256 SBTID) external returns (bool); + +``` + +#### Events + +```solidity + /** + * standardChanged + * @notice standardChanged MUST be triggered when claims are changed by the admin. + * @dev standardChanged MUST also be triggered for the creation of a new SBTID. + e.g : emit StandardChanged(1, Claims(Metadata('age', 'uint', 'age of the person based on the birth date on the legal document' ), ">=", "18"); + is emitted when the Claim condition is changed which allows the certificate holder to call the functions with the modifier, claims that the holder must be equal or more than 18 years old. + */ + event StandardChanged(uint256 SBTID, Claim[] _claims); + + /** + * certified + * @notice certified MUST be triggered when the SBT certificate is given to the certifying address. + * eg: Certified(0xfoo,2); means that wallet holder address `0xfoo` is certified to hold a certificate issued with id 2, and thus can satisfy all the conditions defined by the required interface. + */ + event Certified(address claimer, uint256 SBTID); + + /** + * revoked + * @notice revoked MUST be triggered when the SBT certificate is revoked. + * eg: Revoked( 0xfoo,1); means that entity user 0xfoo has been revoked to all the function access defined by the SBT ID 1. + */ + event Revoked(address claimer, uint256 SBTID); +} +``` + +## Rationale + +TBD + +## Backwards Compatibility + +- This EIP is backward compliant for the contracts that keep intact the metadata structure of previous issued SBT's with their ID and claim requirement details. + - For e.g if the DeFI provider (using the modifiers to validate the ownership of required SBT by owner) wants the admin to change the logic of verification or remove certain claim structure, the previous holders of the certificates will be affected by these changes. + +## Test Cases + +Test cases for the minimal reference implementation can be found [here](../assets/eip-5851/contracts/test.sol) for using transaction verification regarding whether the users hold the tokens or not. Use Remix IDE to compile and test the contracts. + +## Reference Implementation + +The [interface](../assets/eip-5851/contracts/interfaces/IERC5851.sol) is divided into two separate implementations: + +- [EIP-5851 Verifier](../assets/eip-5851/contracts/ERC5851Verifier.sol) is a simple modifier that needs to be imported by functions that are to be only called by holders of the SBT certificates. Then the modifier will call the issuer contract to verifiy if the claimer has the SBT certifcate in question. + +- [EIP-5851 Issuer](../assets/eip-5851/contracts/ERC5851Issuer.sol) is an example of an identity certificate that can be assigned by a KYC controller contract. This is a full implementation of the standard interface. + +## Security Considerations + +1. Implementation of functional interfaces for creating KYC on SBT (i.e `changeStandardClaim()`, `certify()` and `revoke()`) are dependent on the admin role. Thus the developer must insure security of admin role and rotation of this role to the entity entrusted by the KYC attestation service provider and DeFI protocols that are using this attestation service. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5851/contracts/ERC5851Issuer.sol b/assets/eip-5851/contracts/ERC5851Issuer.sol new file mode 100644 index 00000000000000..f6e91ded89560e --- /dev/null +++ b/assets/eip-5851/contracts/ERC5851Issuer.sol @@ -0,0 +1,46 @@ + +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; +import "./interfaces/IERC5851.sol"; + + +abstract contract ERC5851Issuer is IERC5851{ + mapping(uint256 => IERC5851.Claim[]) private _claimMetadata; + mapping(address => mapping(uint256 => bool)) private _SBTVerified; + address public admin; + + constructor() { + admin = msg.sender; + + } + + function ifVerified(address claimmer, uint256 SBTID) public override view returns (bool){ + return(_SBTVerified[claimmer][SBTID]); + } + + function standardClaim(uint256 SBTID) public override view returns (Claim[] memory){ + return(_claimMetadata[SBTID]); + } + + function changeStandardClaim(uint256 SBTID, Claim[] memory _claims) public override returns (bool){ + require(msg.sender == admin); + _claimMetadata[SBTID] = _claims; + emit StandardChanged(SBTID, _claims); + return(true); + } + + function certify(address claimer, uint256 SBTID) public override returns (bool){ + require(msg.sender == admin); + _SBTVerified[claimer][SBTID] = true; + emit Certified(claimer, SBTID); + return(true); + } + + function revoke(address claimer, uint256 SBTID) external override returns (bool){ + require(msg.sender == admin); + _SBTVerified[claimer][SBTID] = false; + emit Revoked(claimer, SBTID); + return(true); + } + +} diff --git a/assets/eip-5851/contracts/ERC5851Verifier.sol b/assets/eip-5851/contracts/ERC5851Verifier.sol new file mode 100644 index 00000000000000..75c663379e4a32 --- /dev/null +++ b/assets/eip-5851/contracts/ERC5851Verifier.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; +import "./interfaces/IERC5851.sol"; + +abstract contract ERC5851Verifier is IERC5851 { + address private _issuer; + + constructor(address issuer) { + _issuer = issuer; + } + + modifier KYCApproved(address claimer, uint256 SBTID) { + IERC5851(_issuer).ifVerified(claimer, SBTID); + _; + } + +} diff --git a/assets/eip-5851/contracts/interfaces/IERC5851.sol b/assets/eip-5851/contracts/interfaces/IERC5851.sol new file mode 100644 index 00000000000000..a045e9cae718d5 --- /dev/null +++ b/assets/eip-5851/contracts/interfaces/IERC5851.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IERC5851{ + + /** Metadata + * + * @param title defines the name of the claim field + * @param kind is the nature of the data (bool,string,address,bytes,..) + * @param description additional information about claim details. + */ + struct Metadata { + string title; + string kind; + string description; + } + + /** Values + * + * @dev Values here can be read and wrote by smartcontract and front-end, cited from [EIP-3475](./eip-3475.md) + */ + struct Values { + string stringValue; + uint uintValue; + address addressValue; + bool boolValue; + } + + /** Claim + * + * Claims structure consist of the conditions and value that holder claims to associate and verifier has to validate them. + * @notice the below given parameters are for reference purposes only, developers can optimize the fields that are needed to be represented on-chain by using schemes like TLV, encoding into base64 etc. + * @dev structure that DeFines the parameters for specific claims of the SBT certificate + * @notice this structure is used for the verification process, it contains the metadata, logic and expectation + * @logic given here MUST be one of ("⊄", "⊂", "<", "<=", "==", "!=", ">=",">") + */ + struct Claim { + Metadata metadata; + string logic; + Values expectation; + } + + //Verifier + /// @notice getter function to validate if the address `claimer` is the holder of the claim Defined by the tokenId `SBTID` + /// @dev it MUST be Defining the logic to fetch the result of the ZK verification (either from). + /// @dev logic given here MUST be one of ("⊄", "⊂", "<", "<=", "==", "!=", ">=", ">") + /// @param claimer is the EOA address that wants to validate the SBT issued to it by the KYC. + /// @param SBTID is the Id of the SBT that user is the claimer. + /// @return true if the assertion is valid, else false + /** + example ifVerified(0xfoo, 1) => true will mean that 0xfoo is the holder of the SBT identity token DeFined by tokenId of the given collection. + */ + function ifVerified(address claimer, uint256 SBTID) external view returns (bool); + + //Issuer + /// @notice getter function to fetch the on-chain identification logic for the given identity holder. + /// @dev it MUST not be defined for address(0). + /// @param SBTID is the Id of the SBT that the user is the claimer. + /// @return the struct array of all the descriptions of condition metadata that is defined by the administrator for the given KYC provider. + /** + ex: standardClaim(1) --> { + { "title":"age", + "kind": "uint", + "description": "age of the person based on the birth date on the legal document", + }, + "logic": ">=", + "value":"18" + } + Defines the condition encoded for the identity index 1, DeFining the identity condition that holder must be equal or more than 18 years old. + **/ + function standardClaim(uint256 SBTID) external view returns (Claim[] memory); + + /// @notice function for setting the claim requirement logic (defined by Claims metadata) details for the given identity token defined by SBTID. + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which the admin wants to define the Claims. + /// @param `claims` is the struct array of all the descriptions of condition metadata that is defined by the administrator. check metadata section for more information. + /** + example: changeStandardClaim(1, { "title":"age", + "kind": "uint", + "description": "age of the person based on the birth date on the legal document", + }, + "logic": ">=", + "value":"18" + }); + will correspond to the functionality that admin needs to adjust the standard claim for the identification SBT with tokenId = 1, based on the conditions described in the Claims array struct details. + **/ + function changeStandardClaim(uint256 SBTID, Claim[] memory _claims) external returns (bool); + + /// @notice function which uses the ZKProof protocol to validate the identity based on the given + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which admin wants to define the Claims. + /// @param claimer is the address that needs to be proven as the owner of the SBT defined by the tokenID. + /** + example: certify(0xA....., 10) means that admin assigns the DID badge with id 10 to the address defined by the `0xA....` wallet. + */ + function certify(address claimer, uint256 SBTID) external returns (bool); + + /// @notice function which uses the ZKProof protocol to validate the identity based on the given + /// @dev it should only be called by the admin address. + /// @param SBTID is the Id of the SBT-based identity certificate for which the admin wants to define the Claims. + /// @param certifying is the address that needs to be proven as the owner of the SBT defined by the tokenID. + // eg: revoke(0xfoo,1): means that KYC admin revokes the SBT certificate number 1 for the address '0xfoo'. + function revoke(address certifying, uint256 SBTID) external returns (bool); + + +// Events + /** + * standardChanged + * @notice standardChanged MUST be triggered when claims are changed by the admin. + * @dev standardChanged MUST also be triggered for the creation of a new SBTID. + e.g : emit StandardChanged(1, Claims(Metadata('age', 'uint', 'age of the person based on the birth date on the legal document'), ">=", "18"); + is emitted when the Claim condition is changed which allows the certificate holder to call the functions with the modifier, claims that the holder must be equal or more than 18 years old. + */ + event StandardChanged(uint256 SBTID, Claim[] _claims); + + /** + * certified + * @notice certified MUST be triggered when the SBT certificate is given to the certifying address. + * eg: Certified(0xfoo,2); means that wallet holder address 0xfoo is certified to hold a certificate issued with id 2, and thus can satisfy all the conditions defined by the required interface. + */ + event Certified(address claimer, uint256 SBTID); + + /** + * revoked + * @notice revoked MUST be triggered when the SBT certificate is revoked. + * eg: Revoked( 0xfoo,1); means that entity user 0xfoo has been revoked to all the function access defined by the SBT ID 1. + */ + event Revoked(address claimer, uint256 SBTID); +} diff --git a/assets/eip-5851/contracts/test.sol b/assets/eip-5851/contracts/test.sol new file mode 100644 index 00000000000000..a299dbb65685d8 --- /dev/null +++ b/assets/eip-5851/contracts/test.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; +import "./ERC5851Verifier.sol"; + +abstract contract Token is ERC5851Verifier { + uint public test; + uint public SBTID; + function mint(address to, uint256 amount) public KYCApproved(to, SBTID){ + _mint(to, amount); + } + + function _mint(address account, uint256 amount) internal { + require(account != address(0), "ERC20: mint to the zero address"); + test = amount; + } + +} diff --git a/assets/eip-5851/script/offchainOperations.js b/assets/eip-5851/script/offchainOperations.js new file mode 100644 index 00000000000000..e2636d32d830d6 --- /dev/null +++ b/assets/eip-5851/script/offchainOperations.js @@ -0,0 +1,69 @@ +// CC0 license. +// taken from (credits): https://soliditydeveloper.com/merkle-tree +const keccak256 = require("keccak256"); +const { MerkleTree } = require("merkletreejs"); + +const Web3 = require("web3"); + +const web3 = new Web3(); + + +/** + * generates the proof offchain using the PII information of the user and associated it with the other parameter details. + * + */ + +async function generateProof() { + +// consider the given information that is verified privately off-chain (storing with wallet, name, age, personal ID, jurisdiction) +// they will be considered as leaves for the application. +const personalIdentifiedInfo = ["0x00000a86986129038908a9808098-toto-18-99123456-France", "0x00000a86986129038908a9808098-john-20-1276546-England"].map(x => keccak256(x)); +const tree = new MerkleTree(leaves,keccak256); + +} + + +/** + * this checks the ownership of the information from requirement (stored onchain) and then verify whether the keccak256 representation is a member of the given proof. + * we follow the checkProof + */ +async function verifyRequirement(verifyingAddress,leafNodes) { +const buf2hex = x => '0x'+x.toString('hex') +const leaf = keccak256('0x00000a86986129038908a9808098-toto-18-99123456-France') + +const leafInfo = buf2hex(leaf); + +const hexproof = tree.getProof(leaf).map(x => buf2hex(x.data)); + +const positions = tree.getProof(leaf).map(x => x.position === 'right' ? 1 : 0) + + + +//TODO: fetch the +const SBTCertification = await web3.eth.Contract(); + +const verifiedOnchain = await SBTCertification.ifVerified(verifyingAddress, leafNodes); + +assert.equal(MerkleTree.verify(proof,leaf,root), verifiedOnchain); + +} + + + +const root = tree.getRoot(); + + +// this is the root generated by claim verifier, by doing the operations offchain. +const hexroot = buf2hex(root); + +const merkleTree = new MerkleTree(leafNodes, keccak256, { sortPairs: true }); + +console.log("---------"); +console.log("Merke Tree"); +console.log("---------"); +console.log(merkleTree.toString()); +console.log("---------"); +console.log("Merkle Root: " + merkleTree.getHexRoot()); + +console.log("Proof 1: " + merkleTree.getHexProof(leafNodes[0])); +console.log("Proof 2: " + merkleTree.getHexProof(leafNodes[1])); From eca64fe83d7df37b3f3b1e2cb512cf6f671d3b20 Mon Sep 17 00:00:00 2001 From: "5660.eth" <76733013+5660-eth@users.noreply.github.com> Date: Sun, 15 Jan 2023 12:12:59 +0800 Subject: [PATCH 170/274] Update eip-6147.md (#6331) --- EIPS/eip-6147.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-6147.md b/EIPS/eip-6147.md index 394dab6a54c06c..1c790503c96523 100644 --- a/EIPS/eip-6147.md +++ b/EIPS/eip-6147.md @@ -147,7 +147,7 @@ pragma solidity ^0.8.8; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "./IERC6147.sol"; -abstract contract ERC721QS is ERC721, IERC6147 { +abstract contract ERC6147 is ERC721, IERC6147 { mapping(uint256 => address) internal token_guard_map; @@ -160,12 +160,12 @@ abstract contract ERC721QS is ERC721, IERC6147 { function updateGuard(uint256 tokenId,address newGuard,bool allowNull) internal { address guard = guardOf(tokenId); if (!allowNull) { - require(newGuard != address(0), "New guard can not be null"); + require(newGuard != address(0), "ERC6147: new guard can not be null"); } if (guard != address(0)) { - require(guard == _msgSender(), "only guard can change it self"); + require(guard == _msgSender(), "ERC6147: only guard can change it self"); } else { - require(_isApprovedOrOwner(_msgSender(), tokenId),"ERC721QS: caller is not owner nor approved"); + require(_isApprovedOrOwner(_msgSender(), tokenId),""ERC6147: caller is not owner nor approved"); } if (guard != address(0) || newGuard != address(0)) { @@ -271,7 +271,7 @@ abstract contract ERC721QS is ERC721, IERC6147 { /// @dev See {IERC165-supportsInterface}. function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { - return interfaceId == type(IERC721QS).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IERC6147).interfaceId || super.supportsInterface(interfaceId); } } From 725c2da2fab4d576a264c485b873abf71904db7f Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sun, 15 Jan 2023 09:04:24 -0500 Subject: [PATCH 171/274] Update EIP-5920: Gas cost update and burning via zero address (#6332) --- EIPS/eip-5920.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index 99922143cdfae2..9a899d93eb924d 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -23,16 +23,17 @@ Currently, to send ether to an address requires you to call a function of that a | Parameter | Value | | ------------------- | ------- | | `PAY_OPCODE` | `0xf9` | -| `BASE_GAS_COST` | `8600` | -| `COLD_GAS_COST` | `11100` | +| `BASE_GAS_COST` | `8500` | +| `WARM_GAS_COST` | `100` | +| `COLD_GAS_COST` | `2500` | | `CREATION_GAS_COST` | `32600` | A new opcode is introduced: `PAY` (`PAY_OPCODE`), which: - Pops two values from the stack: `addr` then `val`. -- Transfers `val` wei to the address `addr`. +- Transfers `val` wei from the executing address to the address `addr`. If `addr` is the zero address, instead, `val` wei is burned from the executing address. -The cost of this opcode is `BASE_GAS_COST` if `addr` is a warm account, `COLD_GAS_COST` if `addr` is a cold account that is already in the state trie, or `BASE_GAS_COST+CREATION_GAS_COST` if `addr` has not yet been added to the state trie. +The cost of this opcode is `BASE_GAS_COST`. If `addr` is not the zero address, this opcode costs an additional `WARM_GAS_COST` if `addr` is a warm account, `COLD_GAS_COST` if `addr` is a cold account, or `CREATION_GAS_COST` if `addr` has not yet been created. ## Rationale From eaabdb7a266df4546eaacd027f6b62aac4919ecf Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sun, 15 Jan 2023 09:08:11 -0500 Subject: [PATCH 172/274] Update EIP-6189: Add support for EIP-5920 (#6333) --- EIPS/eip-6189.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-6189.md b/EIPS/eip-6189.md index 2dc6ccc08550f9..8608ead4dd637e 100644 --- a/EIPS/eip-6189.md +++ b/EIPS/eip-6189.md @@ -33,9 +33,9 @@ A contract is an alias contract if its nonce is `2^64-1`, and its contract code ### Opcode Changes -#### `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`, and EOA Transactions +#### `CALL`, `CALLCODE`, `DELEGATECALL`, `STATICCALL`, `PAY`, and EOA Transactions -The "callee" refers to the account that is being called. +The "callee" refers to the account that is being called or being paid. If the nonce of the callee is `2^64-1`, the call is forwarded to the address stored in the `0`th storage slot of the callee (as if the callee was the address stored in the `0`th storage slot of the callee). This repeats until a non-alias contract is reached. The `CALLER` remains unchanged. From 3f430daecc31005ae99981e1f877c38e76af10c6 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Mon, 16 Jan 2023 06:21:14 -0800 Subject: [PATCH 173/274] Move to Review (#6326) --- EIPS/eip-5269.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5269.md b/EIPS/eip-5269.md index 161cb9ff7a872e..eb1b25ea608d9e 100644 --- a/EIPS/eip-5269.md +++ b/EIPS/eip-5269.md @@ -4,7 +4,7 @@ title: EIP/ERC Detection and Discovery description: An interface to identify if major behavior or optional behavior specified in an ERC is supported for a given caller. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc5269-human-readable-interface-detection/9957 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-07-15 From b56a299fbad4ee701e6d4cea025096effaf301fa Mon Sep 17 00:00:00 2001 From: Alex Stokes Date: Mon, 16 Jan 2023 11:17:11 -0700 Subject: [PATCH 174/274] EIP-4895: CL-EL withdrawals harmonization: using units of Gwei (#6325) * harmonize the CL-EL representation of the withdrawal amounts * Update EIPS/eip-4895.md * harmonize the CL-EL representation of the withdrawal amounts --- EIPS/eip-4895.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-4895.md b/EIPS/eip-4895.md index bf45884c9940f8..0966bbc3ff9212 100644 --- a/EIPS/eip-4895.md +++ b/EIPS/eip-4895.md @@ -41,10 +41,11 @@ Define a new payload-level object called a `withdrawal` that describes withdrawa `Withdrawal`s are syntactically similar to a user-level transaction but live in a different domain than user-level transactions. `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, as a `uint64` value, on the consensus layer the withdrawal corresponds to 3. a recipient for the withdrawn ether `address` as a 20-byte value -4. a nonzero `amount` of ether given in wei as a `uint256` value. +4. a nonzero `amount` of ether given in Gwei (1e9 wei) as a `uint64` value. *NOTE*: the `index` for each withdrawal is a global counter spanning the entire sequence of withdrawals. @@ -129,6 +130,8 @@ The `withdrawals` in an execution payload are processed **after** any user-level For each `withdrawal` in the list of `execution_payload.withdrawals`, the implementation increases the balance of the `address` specified by the `amount` given. +Recall that the `amount` is given in units of Gwei so a conversion to units of wei must be performed when working with account balances in the execution state. + This balance change is unconditional and **MUST** not fail. This operation has no associated gas costs. From e9f53e712fbf6f51d6edcc0c3fd0761d633b99c5 Mon Sep 17 00:00:00 2001 From: Artem Chystiakov <47551140+Arvolear@users.noreply.github.com> Date: Mon, 16 Jan 2023 20:31:55 +0200 Subject: [PATCH 175/274] Add EIP-6224: Contracts Dependencies Registry (#6224) * added EIP: ContractsRegistry the Dependency Injector * updated EIP number * fix typos * fix headings linting errors * updated discussion link * Apply suggestions from code review Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * code review fixes * fix eip name and description * fix discussion link Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-6224.md | 255 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 EIPS/eip-6224.md diff --git a/EIPS/eip-6224.md b/EIPS/eip-6224.md new file mode 100644 index 00000000000000..0f815adc0948bc --- /dev/null +++ b/EIPS/eip-6224.md @@ -0,0 +1,255 @@ +--- +eip: 6224 +title: Contracts Dependencies Registry +description: An interface for managing smart contracts with their dependencies. +author: Artem Chystiakov (@arvolear) +discussions-to: https://ethereum-magicians.org/t/eip-6224-contracts-dependencies-registry/12316 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-27 +requires: 1967, 5750 +--- + +## Abstract + +The EIP standardizes the management of smart contracts within the decentralized application ecosystem. It enables protocols to become upgradeable and reduces their maintenance threshold. This EIP additionally introduces a smart contract dependency injection mechanism to audit dependency usage, to aid larger composite projects. + +## Motivation + +In the ever-growing Ethereum, projects tend to become more and more complex. Modern protocols require portability and agility to satisfy customer needs by continuously delivering new features and staying on pace with the industry. However, the requirement is hard to achieve due to the immutable nature of blockchains and smart contracts. Moreover, the increased complexity and continuous delivery bring bugs and entangle the dependencies between the contracts, making systems less supportable. + +Applications that have a clear facade and transparency upon their dependencies are easier to develop and maintain. The given EIP tries to solve the aforementioned problems by presenting two concepts: the **contracts registry** and the **dependant**. + +The advantages of using the provided pattern might be: + +- Structured smart contracts management via specialized contract. +- Ad-hoc upgradeability provision. +- Runtime smart contracts addition, removal, and substitution. +- Dependency injection mechanism to keep smart contracts' dependencies under control. + +## Specification + +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. + +### ContractsRegistry + +The `ContractsRegistry` MUST implement the following interface: + +```solidity +pragma solidity ^0.8.0; + +interface IContractsRegistry { + /** + * @notice REQUIRED The event that is emitted when the contract gets added to the registry + * @param name the name of the contract + * @param contractAddress the address of the added contract + * @param isProxy whether the added contract is a proxy + */ + event AddedContract(string name, address contractAddress, bool isProxy); + + /** + * @notice REQUIRED The event that is emitted when the contract get removed from the registry + * @param name the name of the removed contract + */ + event RemovedContract(string name); + + /** + * @notice REQUIRED The function that returns an associated contract by the name + * @param name the name of the contract + * @return the address of the contract + */ + function getContract(string memory name) external view returns (address); + + /** + * @notice OPTIONAL The function that checks if a contract with a given name has been added + * @param name the name of the contract + * @return true if the contract is present in the registry + */ + function hasContract(string memory name) external view returns (bool); + + /** + * @notice RECOMMENDED The function that returns the admin of the added proxy contracts + * @return the proxy admin address + */ + function getProxyUpgrader() external view returns (address); + + /** + * @notice RECOMMENDED The function that returns an implementation of the given proxy contract + * @param name the name of the contract + * @return the implementation address + */ + function getImplementation(string memory name) external view returns (address); + + /** + * @notice REQUIRED The function that injects dependencies into the given contract. + * MUST call the setDependencies() with address(this) and bytes("") as arguments on the substituted contract + * @param name the name of the contract + */ + function injectDependencies(string memory name) external; + + /** + * @notice REQUIRED The function that injects dependencies into the given contract with extra data. + * MUST call the setDependencies() with address(this) and given data as arguments on the substituted contract + * @param name the name of the contract + * @param data the extra context data + */ + function injectDependenciesWithData( + string calldata name, + bytes calldata data + ) external; + + /** + * @notice REQUIRED The function that upgrades added proxy contract with a new implementation + * @param name the name of the proxy contract + * @param newImplementation the new implementation the proxy will be upgraded to + * + * It is the Owner's responsibility to ensure the compatibility between implementations + */ + function upgradeContract(string memory name, address newImplementation) external; + + /** + * @notice RECOMMENDED The function that upgrades added proxy contract with a new implementation, providing data + * @param name the name of the proxy contract + * @param newImplementation the new implementation the proxy will be upgraded to + * @param data the data that the new implementation will be called with. This can be an ABI encoded function call + * + * It is the Owner's responsibility to ensure the compatibility between implementations + */ + function upgradeContractAndCall( + string memory name, + address newImplementation, + bytes memory data + ) external; + + /** + * @notice REQUIRED The function that adds pure (non-proxy) contracts to the ContractsRegistry. The contracts MAY either be + * the ones the system does not have direct upgradeability control over or the ones that are not upgradeable by design + * @param name the name to associate the contract with + * @param contractAddress the address of the contract + */ + function addContract(string memory name, address contractAddress) external; + + /** + * @notice REQUIRED The function that adds the contracts and deploys the Transaprent proxy above them. + * It MAY be used to add contract that the ContractsRegistry has to be able to upgrade + * @param name the name to associate the contract with + * @param contractAddress the address of the implementation + */ + function addProxyContract(string memory name, address contractAddress) external; + + /** + * @notice RECOMMENDED The function that adds an already deployed proxy to the ContractsRegistry. It MAY be used + * when the system migrates to the new ContractRegistry. In that case, the new ProxyUpgrader MUST have the + * credentials to upgrade the newly added proxies + * @param name the name to associate the contract with + * @param contractAddress the address of the proxy + */ + function justAddProxyContract(string memory name, address contractAddress) external; + + /** + * @notice REQUIRED The function to remove contracts from the ContractsRegistry + * @param name the associated name with the contract + */ + function removeContract(string memory name) external; +} +``` + +- The `ContractsRegistry` MUST deploy the `ProxyUpgrader` contract in the constructor that MUST be set as an admin of `Transparent` proxies deployed via `addProxyContract` method. +- It MUST NOT be possible to add the zero address to the `ContractsRegistry`. +- The `ContractsRegistry` MUST use the `IDependant` interface in the `injectDependencies` and `injectDependenciesWithData` methods. + +### Dependant + +The `Dependant` contract is the one that depends on other contracts present in the system. In order to support dependency injection mechanism, the dependant contract MUST implement the following interface: + +```solidity +pragma solidity ^0.8.0; + +interface IDependant { + /** + * @notice The function that is called from the ContractsRegistry (or factory) to inject dependencies. + * @param contractsRegistry the registry to pull dependencies from + * @param data the extra data that might provide additional application-specific context/behavior + * + * The Dependant MUST perform a dependency injector access check to this method + */ + function setDependencies(address contractsRegistry, bytes calldata data) external; + + /** + * @notice The function that sets the new dependency injector. + * @param injector the new dependency injector + * + * The Dependant MUST perform a dependency injector access check to this method + */ + function setInjector(address injector) external; + + /** + * @notice The function that gets the current dependency injector + * @return the current dependency injector + */ + function getInjector() external view returns (address); +} +``` + +- The `Dependant` contract MUST pull its dependencies in the `setDependencies` method from the passed `contractsRegistry` address. +- The `Dependant` contract MAY store the dependency injector address in the special slot `0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583` (obtained as `bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)`). + + +## Rationale + +There are a few design decisions that have to be specified explicitly: + +### ContractsRegistry Rationale + +#### Usage + +The extensions of this EIP SHOULD add proper access control checks to the described non-view methods. + +The `getContract` and `getImplementation` methods MUST revert if the nonexistent contracts are queried. + +The `ContractsRegistry` MAY be set behind the proxy to enable runtime addition of custom methods. Applications MAY also leverage the pattern to develop custom tree-like `ContractsRegistry` data structures. + +#### Contracts identifier + +The `string` contracts identifier is chosen over the `uint256` and `bytes32` to maintain code readability and reduce the human-error chances when interacting with the `ContractsRegistry`. Being the topmost smart contract, it MAY be typical for the users to interact with it via block explorers or DAOs. Clarity was prioritized over gas usage. + +#### Proxy + +The `Transparent` proxy is chosen over the `UUPS` proxy to hand the upgradeability responsibility to the `ContractsRegistry` itself. The extensions of this EIP MAY use the proxy of their choice. + +### Dependant Rationale + +#### Dependencies + +The required dependencies MUST be set in the overridden `setDependencies` method, not in the `constructor` or `initializer` methods. + +The `data` parameter is provided to carry additional application-specific context. It MAY be used to extend the method's behavior. + +#### Injector + +Only the injector MUST be able to call the `setDependencies` and `setInjector` methods. The initial injector will be a zero address, in that case, the call MUST NOT revert on access control checks. The `setInjector` function is made `external` to support the dependency injection mechanism for factory-made contracts. However, the method SHOULD be used with extra care. + +The injector address MAY be stored in the dedicated slot `0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583` to exclude the chances of storage collision. + +## Reference Implementation + +*0xdistributedlab-solidity-library dev-modules* provides a reference implementation. + +## Security Considerations + +The described EIP must be used with extra care as the loss/leakage of credentials to the `ContractsRegistry` leads to the application's point of no return. The `ContractRegistry` is a cornerstone of the protocol, access must be granted to the trusted parties only. + +### ContractsRegistry Security Considerations + +- The non-view methods of `ContractsRegistry` contract MUST be overridden with proper access control checks. +- The `ContractsRegistry` does not perform any upgradeability checks between the proxy upgrades. It is the user's responsibility to make sure that the new implementation is compatible with the old one. + +### Dependant Security Considerations + +- The non-view methods of `Dependant` contract MUST be overridden with proper access control checks. Only the dependency injector MUST be able to call them. +- The `Dependant` contract MUST set its dependency injector no later than the first call to the `setDependencies` function is made. That being said, it is possible to front-run the first dependency injection. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 9dbb2489c2f5ff751ceb1b3505f854f3679d83bc Mon Sep 17 00:00:00 2001 From: Alex Beregszaszi Date: Tue, 17 Jan 2023 03:23:49 +0100 Subject: [PATCH 176/274] EIP-4844: Explicitly state sha256 is used (#6345) --- EIPS/eip-4844.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index b380662ef7088a..fbf8bd3c4f6087 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -82,7 +82,7 @@ Specifically, we use the following methods from [`polynomial-commitments.md`](ht ```python def kzg_to_versioned_hash(kzg: KZGCommitment) -> VersionedHash: - return BLOB_COMMITMENT_VERSION_KZG + hash(kzg)[1:] + return BLOB_COMMITMENT_VERSION_KZG + sha256(kzg)[1:] ``` Approximates `factor * e ** (numerator / denominator)` using Taylor expansion: From 739e75c93b94fc49e8005943d052fa4e1ac1be80 Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 17 Jan 2023 17:09:47 +0100 Subject: [PATCH 177/274] Update EIP-4844: clarify datahash return value (#6348) It is replaced with the input on the stack. --- EIPS/eip-4844.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4844.md b/EIPS/eip-4844.md index fbf8bd3c4f6087..ef61aa9370fe7a 100644 --- a/EIPS/eip-4844.md +++ b/EIPS/eip-4844.md @@ -243,9 +243,9 @@ The `ethereum/consensus-specs` repository defines the following beacon-node chan ### Opcode to get versioned hashes -We add an opcode `DATAHASH` (with byte value `HASH_OPCODE_BYTE`) which takes as input one stack argument `index`, -and returns `tx.message.blob_versioned_hashes[index]` if `index < len(tx.message.blob_versioned_hashes)`, -and otherwise zero. +We add an opcode `DATAHASH` (with byte value `HASH_OPCODE_BYTE`) which reads `index` from the top of the stack +as big-endian `uint256`, and replaces it on the stack with `tx.message.blob_versioned_hashes[index]` +if `index < len(tx.message.blob_versioned_hashes)`, and otherwise with a zeroed `bytes32` value. The opcode has a gas cost of `HASH_OPCODE_GAS`. ### Point evaluation precompile From fe8f0223dbd730d6996bfe2a172de0ec775463b5 Mon Sep 17 00:00:00 2001 From: Leeren Date: Tue, 17 Jan 2023 14:11:34 -0800 Subject: [PATCH 178/274] Add EIP-5700: Bindable Token Standard (#5700) * Add EIP-5700: Bindable Token Standard * Finalizes EIP draft * Adds implementation references * Fixes improper file naming * Fixes final lint issue * Ensures all assets are CC0 licensed * Removes watermarks * Fixes misplaced tab characters * Fixes last linting bugs * small lint --- EIPS/eip-5700.md | 826 ++++++++++++++++++ assets/eip-5700/erc1155/ERC1155.sol | 192 ++++ assets/eip-5700/erc1155/ERC1155Bindable.sol | 239 +++++ assets/eip-5700/erc1155/ERC1155Binder.sol | 108 +++ assets/eip-5700/erc721/ERC721.sol | 232 +++++ assets/eip-5700/erc721/ERC721Bindable.sol | 210 +++++ assets/eip-5700/erc721/ERC721Binder.sol | 135 +++ .../eip-5700/interfaces/IERC1155Bindable.sol | 239 +++++ .../interfaces/IERC1155BindableErrors.sol | 13 + assets/eip-5700/interfaces/IERC1155Binder.sol | 130 +++ .../interfaces/IERC1155BinderErrors.sol | 13 + assets/eip-5700/interfaces/IERC1155Errors.sol | 28 + .../eip-5700/interfaces/IERC721Bindable.sol | 112 +++ .../interfaces/IERC721BindableErrors.sol | 19 + assets/eip-5700/interfaces/IERC721Binder.sol | 72 ++ .../interfaces/IERC721BinderErrors.sol | 25 + assets/eip-5700/interfaces/IERC721Errors.sol | 29 + 17 files changed, 2622 insertions(+) create mode 100644 EIPS/eip-5700.md create mode 100644 assets/eip-5700/erc1155/ERC1155.sol create mode 100644 assets/eip-5700/erc1155/ERC1155Bindable.sol create mode 100644 assets/eip-5700/erc1155/ERC1155Binder.sol create mode 100644 assets/eip-5700/erc721/ERC721.sol create mode 100644 assets/eip-5700/erc721/ERC721Bindable.sol create mode 100644 assets/eip-5700/erc721/ERC721Binder.sol create mode 100644 assets/eip-5700/interfaces/IERC1155Bindable.sol create mode 100644 assets/eip-5700/interfaces/IERC1155BindableErrors.sol create mode 100644 assets/eip-5700/interfaces/IERC1155Binder.sol create mode 100644 assets/eip-5700/interfaces/IERC1155BinderErrors.sol create mode 100644 assets/eip-5700/interfaces/IERC1155Errors.sol create mode 100644 assets/eip-5700/interfaces/IERC721Bindable.sol create mode 100644 assets/eip-5700/interfaces/IERC721BindableErrors.sol create mode 100644 assets/eip-5700/interfaces/IERC721Binder.sol create mode 100644 assets/eip-5700/interfaces/IERC721BinderErrors.sol create mode 100644 assets/eip-5700/interfaces/IERC721Errors.sol diff --git a/EIPS/eip-5700.md b/EIPS/eip-5700.md new file mode 100644 index 00000000000000..0de0f5c8ae402c --- /dev/null +++ b/EIPS/eip-5700.md @@ -0,0 +1,826 @@ +--- +eip: 5700 +title: Bindable Token Interface +description: Interface for binding fungible and non-fungible tokens to assets. +author: Leeren (@leeren) +discussions-to: https://ethereum-magicians.org/t/eip-5700-bindable-token-standard/11077 +status: Draft +type: Standards Track +category: ERC +created: 2022-09-22 +requires: 165, 721, 1155 +--- + +## Abstract + +The proposed standard defines an interface by which fungible and non-fungible tokens may be bound to arbitrary assets (typically represented as NFTs themselves), enabling token ownership and transfer attribution to be proxied through the assets they are bound to. + +A bindable token ("bindable") is an [EIP-721](./eip-721.md) or [EIP-1155](./eip-1155.md) token which, when bound to an asset, delegates ownership and tracking through its bound asset, remaining locked for direct transfers until it is unbound. When unbound, bindable tokens function normally according to their base token implementations. + +A bound asset ("binder") has few restrictions on how it is represented, except that it be unique and expose an interface for ownership queries. A binder would most commonly be represented as an [EIP-721](./eip-721.md) NFT. Binders and bindables form a one-to-many relationship. + +Below are example use-cases that benefit from such a standard: + +- NFT-bundled physical assets: microchipped streetwear bundles, digitized automobile collections, digitally-twinned real-estate property +- NFT-bundled digital assets: accessorizable virtual wardrobes, composable music tracks, customizable metaverse land + +## Motivation + +A standard interface for token binding allows tokens to be bundled and transferred with other assets in a way that is easily integrable with wallets, marketplaces, and other NFT applications, and avoids the need for ad-hoc ownership attribution strategies that are neither flexible nor backwards-compatible. + +Unlike other standards tackling delegated ownership attribution, which look at composability on the account level, this standard addresses composability on the asset level, with the goal of creating a universal interface for token modularity that is compatible with existing [EIP-721](./eip-721.md) and [EIP-1155](./eip-1155.md) 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. + +### EIP-721 Bindable + +**Smart contracts implementing the EIP-721 bindable standard MUST implement the `IERC721Bindable` interface.** + +**Implementers of the `IER721Bindable` interface MUST return `true` if `0x82a34a7d` is passed as the identifier to the `supportsInterface` function.** + +```solidity +/// @title ERC-721 Bindable Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-5700 +/// Note: the ERC-165 identifier for this interface is 0x82a34a7d. +interface IERC721Bindable /* is IERC721 */ { + + /// @notice The `Bind` event MUST emit when NFT ownership is delegated + /// through an asset and when minting an NFT bound to an existing asset. + /// @dev When minting bound NFTs, `from` MUST be set to the zero address. + /// @param operator The address calling the bind. + /// @param from The unbound NFT owner address. + /// @param to The bound NFT owner delegate address. + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + event Bind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `Unbind` event MUST emit when asset-bound NFT ownership is + /// revoked, as well as when burning an NFT bound to an existing asset. + /// @dev When burning bound NFTs, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind. + /// @param from The bound asset owner address. + /// @param to The unbound NFT owner address. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event Unbind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice Binds NFT `tokenId` owned by `from` to asset `bindId` at address + /// `bindAddress`, delegating NFT-bound ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is the current owner, + /// an authorized operator, or the approved address for the NFT. It also + /// MUST throw if the NFT is already bound, if `from` is not the NFT owner, + /// or if `to` is not `bindAddress` or its asset owner. After binding, the + /// function MUST check if `bindAddress` is a valid contract / (code size + /// > 0), and if so, call `onERC721Bind` on it, throwing if the wrong + /// identifier is returned (see "Binding Rules") or if the contract is + /// invalid. On bind completion, the function MUST emit `Bind` & `Transfer` + /// events to reflect delegated ownership change. + /// @param from The unbound NFT original owner address. + /// @param to The bound NFT delegate owner address (SHOULD be `bindAddress`). + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC721Bind` hook. + function bind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Unbinds NFT `tokenId` from asset `bindId` owned by `from` at + /// address `bindAddress`, assigning unbound NFT ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is the asset owner or + /// an approved operator. It also MUST throw if NFT `tokenId` is not bound, + /// if `from` is not the asset owner, or if `to` is the zero address. After + /// unbinding, the function MUST check if `bindAddress` is a valid contract + /// (code size > 0), and if so, call `onERC721Unbind` on it, throwing if + /// the wrong identifier is returned (see "Binding Rules") or if the + /// contract is invalid. The function also MUST check if `to` is a valid + /// contract, and if so, call `onERC721Received`, throwing if the wrong + /// identifier is returned. On unbind completion, the function MUST emit + /// `Unbind` & `Transfer` events to reflect delegated ownership change. + /// @param from The bound asset owner address. + /// @param to The unbound NFT owner address. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC721Unbind` hook. + function unbind( + address from, + address to, + uint256 tokenId, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Gets the asset identifier and address which an NFT is bound to. + /// @param tokenId The identifier of the NFT being queried. + /// @return The bound asset identifier and contract address. + function binderOf(uint256 tokenId) external returns (uint256, address); + + /// @notice Counts NFTs bound to asset `bindId` at address `bindAddress`. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param bindId The identifier of the bound asset. + /// @return The total number of NFTs bound to the asset. + function boundBalanceOf(address bindAddress, uint256 bindId) external returns (uint256); + +``` + +**Smart contracts managing assets MUST implement the `IERC721Binder` interface if they are to accept binds from EIP-721 bindables.** + +**Implementers of the `IERC721Binder` interface MUST return `true` if `0x2ac2d2bc` is passed as the identifier to the `supportsInterface` function.** + +```solidity +/// @dev Note: the ERC-165 identifier for this interface is 0x2ac2d2bc. +interface IERC721Binder /* is IERC165 */ { + + /// @notice Handles the binding of an IERC721Bindable-compliant NFT. + /// @dev An IERC721Bindable-compliant smart contract MUST call this function + /// at the end of a `bind` after ownership is delegated through an asset. + /// The function MUST revert if `to` is not the asset owner or the binder + /// address. The function MUST revert if it rejects the bind. If accepting + /// the bind, the function MUST return `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding NFT is `msg.sender`. + /// @param operator The address initiating the bind. + /// @param from The unbound NFT owner address. + /// @param to The bound NFT owner delegate address. + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))` + function onERC721Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles the unbinding of an IERC721Bindable-compliant NFT. + /// @dev An IERC721Bindable-compliant smart contract MUST call this function + /// at the end of an `unbind` after revoking asset-delegated ownership. + /// The function MUST revert if `from` is not the asset owner of `bindId`. + /// The function MUST revert if it rejects the unbind. If accepting the + /// unbind, the function MUST return `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding NFT is `msg.sender`. + /// @param from The bound asset owner address. + /// @param to The unbound NFT owner address. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param data Additional data with no specified format. + /// @return `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))` + function onERC721Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Gets the owner address of the asset identified by `bindId`. + /// @dev This function MUST throw for assets assigned to the zero address. + /// @param bindId The identifier of the asset whose owner is being queried. + /// @return The address of the owner of the asset. + function ownerOf(uint256 bindId) external view returns (address); + + /// @notice Checks if an operator can act on behalf of an asset owner. + /// @param owner The address that owns an asset. + /// @param operator The address that can act on behalf of the asset owner. + /// @return True if `operator` can act on behalf of `owner`, else False. + function isApprovedForAll(address owner, address operator) external view returns (bool); + +} +``` + +### EIP-1155 Bindable + +**Smart contracts implementing the EIP-1155 Bindable standard MUST implement the `IERC1155Bindable` interface.** + +**Implementers of the `IER1155Bindable` interface MUST return `true` if `0xd0d55c6` is passed as the identifier to the `supportsInterface` function.** + +```solidity +/// @title ERC-1155 Bindable Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-5700 +/// Note: the ERC-165 identifier for this interface is 0xd0d555c6. +interface IERC1155Bindable /* is IERC1155 */ { + + /// @notice The `Bind` event MUST emit when token ownership is delegated + /// through an asset and when minting tokens bound to an existing asset. + /// @dev When minting bound tokens, `from` MUST be set to the zero address. + /// @param operator The address calling the bind. + /// @param from The owner address of the unbound tokens. + /// @param to The delegate owner address of the bound tokens. + /// @param tokenId The identifier of the token type being bound. + /// @param amount The number of tokens of type `tokenId` being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + event Bind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `BindBatch` event MUST emit when token ownership of + /// different token types are delegated through multiple assets and when + /// minting different token types bound to multiple existing assets. + /// @dev When minting bound tokens, `from` MUST be set to the zero address. + /// @param operator The address calling the bind. + /// @param from The owner address of the unbound tokens. + /// @param to The delegate owner address of the bound tokens. + /// @param tokenIds The identifiers of the token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param bindAddress The contract address handling asset ownership. + event BindBatch( + address indexed operator, + address indexed from, + address to, + uint256[] tokenIds, + uint256[] amounts, + uint256[] bindIds, + address indexed bindAddress + ); + + /// @notice The `Unbind` event MUST emit when asset-delegated token + /// ownership is revoked and when burning tokens bound to existing assets. + /// @dev When burning bound tokens, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenId The identifier of the token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event Unbind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `UnbindBatch` event MUST emit when asset-delegated token + /// ownership is revoked for different token types and when burning + /// different token types bound to multiple existing assets. + /// @dev When burning bound tokens, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenIds The identifiers of the token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifiers of the assets being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event UnbindBatch( + address indexed operator, + address indexed from, + address to, + uint256[] tokenIds, + uint256[] amounts, + uint256[] bindIds, + address indexed bindAddress + ); + + /// @notice Binds `amount` tokens of type `tokenId` owned by `from` to asset + /// `bindId` at `bindAddress`, delegating token-bound ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// for `from`. The function also MUST throw if `from` owns fewer than + /// `amount` tokens, or if `to` is not `bindAddress` or its asset owner. + /// After binding, the function MUST check if `bindAddress` is a valid + /// contract (code size > 0), and if so, call `onERC1155Bind` on it, + /// throwing if the wrong identifier is returned (see "Binding Rules") or + /// if the contract is invalid. On bind completion, the function MUST emit + /// `Bind` & `TransferSingle` events to reflect delegated ownership change. + /// @param from The owner address of the unbound tokens. + /// @param to The delegate owner address of the bound tokens (SHOULD be `bindAddress`). + /// @param tokenId The identifier of the token type being bound. + /// @param amount The number of tokens of type `tokenId` being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC1155Bind` hook. + function bind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Binds `amounts` tokens of types `tokenIds` owned by `from` to + /// assets `bindIds` at `bindAddress`, delegating bound ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// for `from`. The function also MUST throw if length of `amounts` is not + /// the same as `tokenIds` or `bindIds`, if any balances of `tokenIds` for + /// `from` is less than that of `amounts`, or if `to` is not `bindAddress` + /// or the asset owner. After delegating ownership, the function MUST check + /// if `bindAddress` is a valid contract (code size > 0), and if so, call + /// `onERC1155BatchBind` on it, throwing if the wrong identifier is + /// returned (see "Binding Rules") or if the contract is invalid. On bind + /// completion, the function MUST emit `BindBatch` & `TransferBatch` events + /// to reflect delegated ownership changes. + /// @param from The owner address of the unbound tokens. + /// @param to The delegate owner address of the bound tokens (SHOULD be `bindAddress`). + /// @param tokenIds The identifiers of the token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC1155BatchBind` hook. + function batchBind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Revokes delegated ownership of `amount` tokens of type `tokenId` + /// owned by `from` bound to `bindId`, switching ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is the asset owner or + /// an approved operator. It also MUST throw if `from` is not the asset + /// owner, if fewer than `amount` tokens are bound to the asset, or if `to` + /// is the zero address. Once delegated ownership is revoked, the function + /// MUST check if `bindAddress` is a valid contract (code size > 0), and if + /// so, call `onERC1155Unbind` on it, throwing if the wrong identifier is + /// returned (see "Binding Rules") or if the contract is invalid. The + /// function also MUST check if `to` is a contract, and if so, call on it + /// `onERC1155Received`, throwing if the wrong identifier is returned. On + /// unbind completion, the function MUST emit `Unbind` & `TransferSingle` + /// events to reflect delegated ownership change. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenId The identifier of the token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC1155Unbind` hook. + function unbind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Revokes delegated ownership of `amounts` tokens of `tokenIds` + /// owned by `from` bound to assets `bindIds`, switching ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is the assets' owner or + /// approved operator. It also MUST throw if the length of `amounts` is not + /// the same as `tokenIds` or `bindIds`, if `from` is not the owner of all + /// assets, if any count in `amounts` is fewer than the number of tokens + /// bound for the corresponding token-asset pair given by `tokenIds` and + /// `bindIds`, or if `to` is the zero address. Once delegated ownership is + /// revoked for all tokens, the function MUST check if `bindAddress` is a + /// valid contract (code size > 0), and if so, call `onERC1155BatchUnbind` + /// on it, throwing if a wrong identifier is returned (see "Binding Rules") + /// or if the contract is invalid. The function also MUST check if `to` is + /// valid contract, and if so, call `onERC1155BatchReceived` on it, + /// throwing if the wrong identifier is returned. On unbind completion, the + /// function MUST emit `BatchUnbind` and `TransferBatch` events to reflect + /// delegated ownership change. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenIds The identifiers of the token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifier of the assets being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC1155BatchUnbind` hook. + function batchUnbind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Gets the balance of bound tokens of type `tokenId` bound to the + /// asset `bindId` at address `bindAddress`. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param bindId The identifier of the bound asset. + /// @param tokenId The identifier of the counted bound token type. + /// @return The total number of tokens of type `tokenId` bound to the asset. + function boundBalanceOf( + address bindAddress, + uint256 bindId, + uint256 tokenId + ) external returns (uint256); + + /// @notice Gets the balance of bound tokens for multiple token types given + /// by `tokenIds` bound to assets `bindIds` at address `bindAddress`. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param bindIds List of bound asset identifiers. + /// @param tokenIds The identifiers of the counted bound token types. + /// @return balances The bound balances for each asset / token type pair. + function boundBalanceOfBatch( + address bindAddress, + uint256[] calldata bindIds, + uint256[] calldata tokenIds + ) external returns (uint256[] memory balances); + +} +``` + +**Smart contracts managing assets MUST implement the `IERC1155Binder` interface if they are to accept binds from EIP-1155 bindables.** + +**Implementers of the `IERC1155Binder` interface MUST return `true` if `0x6fc97e78` is passed as the identifier to the `supportsInterface` function.** + +```solidity +pragma solidity ^0.8.16; + +/// @dev Note: the ERC-165 identifier for this interface is 0x6fc97e78. +interface IERC1155Binder /* is IERC165 */ { + + /// @notice Handles binding of an IERC1155Bindable-compliant token type. + /// @dev An IERC1155Bindable-compliant smart contract MUST call this + /// function at the end of a `bind` after ownership is delegated through an + /// asset. The function MUST revert if `to` is not the asset owner or + /// binder address. The function MUST revert if it rejects the bind. If + /// accepting the bind, the function MUST return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding token is `msg.sender`. + /// @param operator The address responsible for binding. + /// @param from The owner address of the unbound tokens. + /// @param to The delegate owner address of the bound tokens. + /// @param tokenId The identifier of the token type being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))` + function onERC1155Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles binding of multiple IERC1155Bindable-compliant tokens + /// `tokenIds` to multiple assets `bindIds`. + /// @dev An IERC1155Bindable-compliant smart contract MUST call this + /// function at the end of a `batchBind` after delegating ownership of + /// multiple token types to the asset owner. The function MUST revert if + /// `to` is not the asset owner or binder address. The function MUST revert + /// if it rejects the bind. If accepting the bind, the function MUST return + /// `bytes4(keccak256("onERC1155BatchBind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding token is `msg.sender`. + /// @param operator The address responsible for performing the binds. + /// @param from The unbound tokens' original owner address. + /// @param to The bound tokens' delegate owner address (SHOULD be `bindAddress`). + /// @param tokenIds The list of token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + function onERC1155BatchBind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles unbinding of an IERC1155Bindable-compliant token type. + /// @dev An IERC1155Bindable-compliant contract MUST call this function at + /// the end of an `unbind` after revoking delegated asset ownership. The + /// function MUST revert if `from` is not the asset owner. The function + /// MUST revert if it rejects the unbind. If accepting the unbind, the + /// function MUST return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding token is `msg.sender`. + /// @param operator The address responsible for performing the unbind. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenId The token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))` + function onERC1155Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles unbinding of multiple IERC1155Bindable-compliant token types. + /// @dev An IERC1155Bindable-compliant contract MUST call this function at + /// the end of a `batchUnbind` after revoking asset-delegated ownership. + /// The function MUST revert if `from` is not the asset owner. The function + /// MUST revert if it rejects the unbinds. If accepting the unbinds, the + /// function MUST return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding token is `msg.sender`. + /// @param operator The address responsible for performing the unbinds. + /// @param from The owner address of the bound asset. + /// @param to The owner address of the unbound tokens. + /// @param tokenIds The list of token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifiers of the assets being unbound from. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + function onERC1155BatchUnbind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) external returns (bytes4); + + /// @notice Gets the owner address of the asset represented by id `bindId`. + /// @param bindId The identifier of the asset whose owner is being queried. + /// @return The address of the owner of the asset. + function ownerOf(uint256 bindId) external view returns (address); + + /// @notice Checks if an operator can act on behalf of an asset owner. + /// @param owner The owner address of an asset. + /// @param operator The address operating on behalf of the asset owner. + /// @return True if `operator` can act on behalf of `owner`, else False. + function isApprovedForAll(address owner, address operator) external view returns (bool); + +} +``` + +### Rules + +This standard supports two modes of binding, depending on whether ownership is delegated to the asset owner or binder address. + +- _Delegated (RECOMMENDED):_ + - Bindable ownership is delegated to the binder address (`to` is `bindAddress` in a bind). + - Bindable ownership queries return the binder address. + - Bindable transfers MUST always throw. +- _Legacy (NOT RECOMMENDED):_ + - Bindable ownership is delegated to the asset owner address (`to` is the asset owner address in a bind). + - Bindable ownership queries return the asset owner address. + - Bindable transfers MUST always throw, except when invoked as a result of bound assets being transferred. + - Transferrable bound assets MUST keep track of bound tokens following this binding mode. + - On transfer, bound assets MUST invoke ownership transfers for bound tokens following this binding mode. + +_Binders SHOULD choose to only support the "delegated" binding mode by throwing if `to` is not `bindAddress`, otherwise both modes MAY be supported._ + +**_`bind` rules:_** + +- When binding an EIP-721 bindable to an asset: + - MUST throw if caller is not the current NFT owner, the approved address for the NFT, or an approved operator for `from`. + - MUST throw if NFT `tokenId` is already bound. + - MUST throw if `from` is not the NFT owner. + - MUST throw if `to` is not `bindAddress` or the asset owner. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC721Bind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit the `Bind` event to reflect asset-bound ownership delegation. + - MUST emit the `Transfer` event if `from` is different than `to` to reflect delegated ownership change. +- When binding an EIP-1155 bindable to an asset: + - MUST throw if caller is not an approved operator for `from`. + - MUST throw if `from` owns fewer than `amount` unbound tokens of type `tokenId`. + - MUST throw if `to` is not `bindAddress` or the asset owner. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC1155Bind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit the `Bind` event to reflect asset-bound ownership delegation. + - MUST emit the `TransferSingle` event if `from` is different than `to` to reflect delegated ownership change. + +**_`unbind` rules:_** + +- When unbinding an EIP-721 bindable from an asset: + - MUST throw if caller is not the owner of the asset or an approved asset operator for `from`. + - MUST throw if NFT `tokenId` is not bound. + - MUST throw if `from` is not the asset owner. + - MUST throw if `to` is the zero address. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC721Unbind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - In addition, it MUST check if `to` is a smart contract (code size > 0), and call `onERC721Received` on `to` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit the `Unbind` event to reflect asset-bound ownership revocation. + - MUST emit the `Transfer` event if `from` is different than `to` to reflect delegated ownership change. +- When unbinding a an EIP-1155 bindable from an asset: + - MUST throw if caller is not the owner of the asset or an approved asset operator for `from`. + - MUST throw if `from` is not the asset owner. + - MUST throw if fewer than `amount` tokens of type `tokenId` are bound to `bindId`. + - MUST throw if `to` is the zero address. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC1155Unbind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - In addition, it MUST check if `to` is a smart contract (code size > 0), and call `onERC1155Received` on `to` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit the `Unbind` event to reflect asset-bound ownership revocation. + - MUST emit the `TransferSingle` event if `from` is different than `to` to reflect delegated ownership change. + +**_`batchBind` & `batchUnbind` rules:_** + +- When performing a `batchBind` on EIP-1155 bindables: + - MUST throw if caller is not an approved operator for `from`. + - MUST throw if length of `tokenIds` is not the same as that of `amounts` or `bindIds`. + - MUST throw if any unbound token balances of `tokenIds` for `from` are less than that of `amounts`. + - MUST throw if `to` is not `bindAddress` or the asset owner. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC1155BatchBind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit either `Bind` or `BindBatch` events to properly reflect asset-delegated ownership attribution for all bound tokens. + - MUST emit either `TransferSingle` or `TransferBatch` events if `from` is different than `to` to reflect delegated ownership changes for all tokens. +- When performing a `batchUnbind` on EIP-1155 bindables: + - MUST throw if caller is not the owner of all assets or an approved asset operator for `from`. + - MUST throw if length of `tokenIds` is not the same as that of `amounts` or `bindIds`. + - MUST throw if `from` is not the owner of all assets. +- MUST throw if any count in `amounts` is fewer than the number of tokens bound for the corresponding token-asset pair given by `tokenIds` and `bindIds`. + - MUST throw if `to` is the zero address. + - After above conditions are met, MUST check if `bindAddress` is a smart contract (code size > 0). If so, it MUST call `onERC1155Unbind` on `bindAddress` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - In addition, it MUST check if `to` is a smart contract (code size > 0), and call `onERC1155Received` on `to` with `data` passed unaltered and act appropriately (see "Hook Rules"). + - MUST emit `Bind` event to reflect asset-bound ownership revocation. + - MUST emit the `TransferSingle` event if `from` is different than `to` to reflect delegated ownership change. + +**_`Bind` event rules:_** + +- When emitting an EIP-721 bindable `Bind` event: + - SHOULD be emitted to indicate a single bind has occurred between a `tokenId` and `bindId` pair. + - MAY be emitted multiple times to indicate multiple binds have occurred in a single transaction. + - The `operator` argument MUST be the owner of the NFT `tokenId`, the approved address for the NFT, or the authorized operator of `from`. + - The `from` argument MUST be the owner of the NFT `tokenId`. + - The `to` argument MUST be `binderAddress` (indicates "delegated" bind) or the owner of the bound asset (indicates "legacy" bind). + - The `tokenId` argument MUST be the NFT being bound. + - The `bindId` argument MUST be the identifier of the asset being bound to. + - The `bindAddress` argument MUST be the contract address of the asset being bound to. + - When minting NFTs bound to an asset, the `Bind` event must be emitted with the `from` argument set to `0x0`. + - `Bind` events MUST be emitted to reflect asset-bound ownership delegation before calls to `onERC721Bind`. +- When emitting an EIP-1155 bindable `Bind` event: + - SHOULD be emitted to indicate a bind has occurred between a single `tokenId` type and `binderId` pair. + - MAY be emitted multiple times to indicate multiple binds have occurred in a single transaction, but `BindBatch` should be preferred in this case to reduce gas consumption. + - The `operator` argument MUST be an authorized operator for `from`. + - The `from` argument MUST be the owner of the unbound tokens. + - The `to` argument MUST be `binderAddress` (indicates "delegated" bind) or the owner of the bound asset `bindId` (indicates "legacy" bind). + - The `tokenId` argument MUST be the token type being bound. + - The `amount` argument MUST be the number of tokens of type `tokenId` being bound. + - The `bindId` argument MUST be the identifier of the asset being bound to. + - The `bindAddress` argument MUST be the contract address of the asset being bound to. + - When minting NFTs bound to an asset, the `Bind` event must be emitted with the `from` argument set to `0x0`. + - `Bind` events MUST be emitted to reflect asset-bound ownership delegation before calls to `onERC1155Bind` or `onERC1155BindBatch`. + +**_`Unbind` event rules:_** + +- When emitting an EIP-721 bindable `Unbind` event: + - SHOULD be emitted to indicate a single unbind has occurred between a `tokenId` and `bindId` pair. + - MAY be emitted multiple times to indicate multiple unbinds have occurred in a single transaction. + - The `operator` argument MUST be the owner of the asset or an approved asset operator for `from`. + - The `from` argument MUST be the owner of the asset. + - The `to` argument MUST be the recipient address of the unbound NFT. + - The `tokenId` argument MUST be the NFT being unbound. + - The `bindId` argument MUST be the identifier of the asset being unbound from. + - The `bindAddress` argument MUST be the contract address of the asset being unbound from. + - When burning NFTs bound to an asset, the `Bind` event must be emitted with the `to` argument set to `0x0`. + - `Bind` events MUST be emitted to reflect delegated ownership revocation changes before calls to `onERC721Unbind`. +- When emitting an EIP-1155 bindable `Unbind` event: + - SHOULD be emitted to indicate an unbind has occurred between a single `tokenId` type and `binderId` pair. + - MAY be emitted multiple times to indicate multiple unbinds have occurred in a single transaction, but `UnbindBatch` should be preferred in this case to reduce gas consumption. + - The `operator` argument MUST be the owner of the asset or an approved asset operator for `from`. + - The `from` argument MUST be the asset owner. + - The `to` argument MUST be the recipient address of the unbound tokens. + - The `tokenId` argument MUST be the token type being unbound. + - The `amount` argument MUST be the number of tokens of type `tokenId` being unbound. + - The `bindId` argument MUST be the identifier of the asset being unbound from. + - The `bindAddress` argument MUST be the contract address of the asset being unbound from. + - When burning NFTs bound to an asset, the `Bind` event must be emitted with the `to` argument set to `0x0`. + - `Bind` events MUST be emitted to reflect delegated ownership revocation changes before calls to `onERC1155Unbind` or `onERC1155UnbindBatch` + +**_`BindBatch` & `UnbindBatch` event rules:_** + +- When emitting a `BindBatch` event: + - SHOULD be emitted to indicate a bind has occurred between multiple `tokenId` and `binderId` pairs. + - The `operator` argument MUST be an authorized operator for `from`. + - The `from` argument MUST be the owner of the unbound tokens. + - The `to` argument MUST be `binderAddress` (indicates "delegated" bind) or the owner of the bound asset (indicates "legacy" bind). + - The `tokenIds` argument MUST be the identifiers of the token types being bound. + - The `amounts` argument MUST be the number of tokens for each type in `tokenIds` being bound. + - The `bindIds` argument MUST be the identifiers for all assets being bound to. + - The `bindAddress` argument MUST be the contract address of the assets being bound to. + - When batch minting NFTs bound to an asset, the `BindBatch` event must be emitted with the `from` argument set to `0x0`. + - `BindBatch` events MUST be emitted to reflect asset-bound ownership delegation before calls to `onERC1155BindBatch` +- When emitting a `batchUnbind` event: + - SHOULD be emitted to indicate an unbind has occurred between multiple `tokenId` and `binderId` pairs. + - The `operator` argument MUST be an authorized operator or owner of the asset. + - The `from` argument MUST be the owner of all assets. + - The `to` argument MUST be the recipient address of the unbound tokens. + - The `tokenIds` argument MUST be the identifiers of the token types being unbound. + - The `amounts` argument MUST be the number of tokens for each type `tokenId` being unbound. + - The `bindIds` argument MUST be the identifiers for the assets being unbound from. + - The `bindAddress` argument MUST be the contract address of the assets being unbound from. + - When burning tokens bound to an asset, the `UnbindBatch` event must be emitted with the `to` argument set to `0x0`. + - `UnbindBatch` events MUST be emitted to reflect asset-delegated ownership changes before calls to `onERC1155UnbindBatch` + +**_`bind` hook rules:_** + +- The `operator` argument MUST be the address calling the bind hook. +- The `from` argument MUST be the owner of the NFT or token type being bound. + - FROM must be `0x0` for a mint. +- The `to` argument MUST be `binderAddress` (indicates "delegated" bind) or the owner of the bound asset (indicates "legacy" bind). + - The binder contract MAY choose to reject legacy binds. +- For `onERC721Bind` / `onERC1155Bind`, the `tokenId` argument MUST be the NFT / token type being bound. +- For `onERC1155BatchBind`, `tokenIds` MUST be the list of token types being bound. +- For `onERC1155Bind`, the `amount` argument MUST be the number of tokens of type `tokenId` being bound. +- For `onERC1155BatchBind`, the `amounts` argument MUST be a list of the number of tokens of each token type being bound. +- For `onERC721Bind` / `onERC1155Bind`, the `bindId` argument MUST be the identifier for the asset being bound to. +- For `onERC1155BatchBind`, `bindIds` MUST be the list of assets being bound to. +- The `data` argument MUST contain data provided by the caller for the bind with contents unaltered. +- The binder contract MAY accept the bind by returning the binder call's designated magic value, in which case the bind MUST complete or revert if any other conditions for success are not met: + - `onERC721Bind`: `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))` + - `onERC1155Bind`: `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))` + - `onERC1155BindBatch`: `bytes4(keccak256("onERC1155BindBatch(address,address,address,uint256[],uint256[],uint256[],bytes)"))` +- The binder contract MAY reject the bind by calling revert. +- A return of any other value than the designated magic value MUST result in the transaction being reverted by the caller. + +**_`unbind` hook rules:_** + +- The `operator` argument MUST be the address calling the unbind hook. +- The `from` argument MUST be the asset owner. +- The `to` argument MUST the the recipient address of the unbound NFT or token type. + - TO must be `0x0` for a burn. +- For `onERC721Unbind` / `onERC1155Unbind`, the `tokenId` argument MUST be the NFT / token type being unbound. +- For `onERC1155BatchUnbind`, `tokenIds` MUST be the list of token types being unbound. +- For `onERC1155Unbind`, the `amount` argument MUST be the number of tokens of type `tokenId` being unbound. +- For `onERC1155BatchUnbind`, the `amounts` argument MUST be a list of the number of tokens of each token type being unbound. +- For `onERC721Bind` / `onERC1155Bind`, the `bindId` argument MUST be the identifier for the asset being unbound from. +- For `onERC1155BatchBind`, `bindIds` MUST be the list of assets being unbound from. +- The `data` argument MUST contain data provided by the caller for the bind with contents unaltered. +- The binder contract MAY accept the unbind by returning the binder call's designated magic value, in which case the unbind MUST complete or MUST revert if any other conditions for success are not met: + - `onERC721Unbind`: `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))` + - `onERC1155Unbind`: `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))` + - `onERC1155UnbindBatch`: `bytes4(keccak256("onERC1155UnbindBatch(address,address,address,uint256[],uint256[],uint256[],bytes)"))` +- The binder contract MAY reject the bind by calling revert. +- A return of any other value than the designated magic value MUST result in the transaction being reverted by the caller. + +## Rationale + +A backwards-compatible standard for token binding unlocks a new layer of composability for allowing wallets, applications, and protocols to interact with, trade and display bundled assets. One example use-case of this is at Dopamine, where microchipped streetwear garments may be bundled with NFTs such as music, avatars, or digital-twins of the garments themselves, by linking chips to binder smart contracts capable of accepting token binds. + +### Binding Mechanism + +In the “delegated” mode, because token ownership is attributed to the contract address of the asset it is bound to, asset ownership modifications are completely decoupled from bound tokens, making bundled transfers efficient as no state management overhead is imposed. This is the recommended binding mode. + +The “legacy” binding mode was included purely for backwards-compatibility purposes, so that existing applications that have yet to integrate the standard can still display bundled tokens out-of-the-box. Here, since token ownership is attributed to the owner of the bound asset, asset ownership modifications are coupled to that of its bound tokens, making bundled transfers inefficient as binder contracts are required to track all bound tokens. + +Binder and bindable implementations MAY choose to support both modes of binding. + +### Transfer Mechanism + +One important consideration was whether binds should support transfers or not. Indeed, it would be much simpler for binds and unbinds to be processed only by addresses who owns both the bindable tokens and assets being bound to. Going this route, binds would not require any dependence on transfers, as asset-delegated ownership would not change, and applications could simply transfer the assets themselves following prescribed asset transfer rules. However, this was ruled out due to the lack of flexibility offered, especially around friction added for consumers wishing to bind their tokens to unowned assets. + +## Backwards Compatibility + +The bindable interface is designed to be compatible with existing EIP-721 and EIP-1155 standards. + +## Reference Implementation + +For reference EIP-721 implementations supporting "delegated" and "legacy" binding modes: + +- [EIP-721 Bindable](../assets/eip-5700/erc721/ERC721Bindable.sol). +- [EIP-721 Binder](../assets/eip-5700/erc721/ERC721Binder.sol). + +For reference EIP-1155 implementations supporting only the "delegated" binding mode: + +- [EIP-1155 Bindable](../assets/eip-5700/erc1155/ERC1155Bindable.sol). +- [EIP-1155 Binder](../assets/eip-5700/erc1155/ERC1155Binder.sol). + +## Security Considerations + +Bindable contracts supporting the "legacy" binding mode should be cautious with authorizing transfers once their tokens are bound. These should only be authorized as a result of their bound assets being transferred, and careful consideration must be taken when ensuring account balances are properly processed. + +Binder contracts supporting the "legacy" binding mode must ensure that any accepted binds are tracked, and that asset transfers result in proper changing of bound token ownership. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-5700/erc1155/ERC1155.sol b/assets/eip-5700/erc1155/ERC1155.sol new file mode 100644 index 00000000000000..652ebb7e7f6729 --- /dev/null +++ b/assets/eip-5700/erc1155/ERC1155.sol @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +import {IERC1155Errors} from "../interfaces/IERC1155Errors.sol"; + +/// @title Dopamine Minimal ERC-1155 Contract +/// @notice This is a minimal ERC-1155 implementation that +contract ERC1155 is IERC1155, IERC1155Errors { + + /// @notice Checks for an owner if an address is an authorized operator. + mapping(address => mapping(address => bool)) public isApprovedForAll; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC1155_INTERFACE_ID = 0xd9b67a26; + + /// @notice Gets an address' number of tokens owned of a specific type. + mapping(address => mapping(uint256 => uint256)) public _balanceOf; + + /// @notice Transfers `amount` tokens of id `id` from address `from` to + /// address `to`, while ensuring `to` is capable of receiving the token. + /// @dev Safety checks are only performed if `to` is a smart contract. + /// @param from The existing owner address of the token to be transferred. + /// @param to The new owner address of the token being transferred. + /// @param id The id of the token being transferred. + /// @param amount The number of tokens being transferred. + /// @param data Additional transfer data to pass to the receiving contract. + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual { + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) { + revert SenderUnauthorized(); + } + + _balanceOf[from][id] -= amount; + _balanceOf[to][id] += amount; + + emit TransferSingle(msg.sender, from, to, id, amount); + + if ( + to.code.length != 0 && + IERC1155Receiver(to).onERC1155Received( + msg.sender, + address(0), + id, + amount, + data + ) != + IERC1155Receiver.onERC1155Received.selector + ) { + revert SafeTransferUnsupported(); + } else if (to == address(0)) { + revert ReceiverInvalid(); + } + } + + /// @notice Transfers tokens `ids` in corresponding batches `amounts` from + /// address `from` to address `to`, while ensuring `to` can receive tokens. + /// @dev Safety checks are only performed if `to` is a smart contract. + /// @param from The existing owner address of the token to be transferred. + /// @param to The new owner address of the token being transferred. + /// @param ids A list of the token ids being transferred. + /// @param amounts A list of the amounts of each token id being transferred. + /// @param data Additional transfer data to pass to the receiving contract. + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual { + if (ids.length != amounts.length) { + revert ArityMismatch(); + } + + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) { + revert SenderUnauthorized(); + } + + uint256 id; + uint256 amount; + + for (uint256 i = 0; i < ids.length; ) { + id = ids[i]; + amount = amounts[i]; + _balanceOf[from][id] -= amount; + _balanceOf[to][id] += amount; + unchecked { + ++i; + } + } + + emit TransferBatch(msg.sender, from, to, ids, amounts); + + if ( + to.code.length != 0 && + IERC1155Receiver(to).onERC1155BatchReceived( + msg.sender, + from, + ids, + amounts, + data + ) != + IERC1155Receiver.onERC1155BatchReceived.selector + ) { + revert SafeTransferUnsupported(); + } else if (to == address(0)) { + revert ReceiverInvalid(); + } + } + + /// @notice Retrieves balance of address `owner` for token of id `id`. + /// @param owner The token owner's address. + /// @param id The id of the token being queried. + /// @return The number of tokens address `owner` owns of type `id`. + function balanceOf(address owner, uint256 id) public view virtual returns (uint256) { + return _balanceOf[owner][id]; + } + + /// @notice Retrieves balances of multiple owner / token type pairs. + /// @param owners List of token owner addresses. + /// @param ids List of token type identifiers. + /// @return balances List of balances corresponding to the owner / id pairs. + function balanceOfBatch(address[] memory owners, uint256[] memory ids) + public + view + virtual + returns (uint256[] memory balances) + { + if (owners.length != ids.length) { + revert ArityMismatch(); + } + + balances = new uint256[](owners.length); + + unchecked { + for (uint256 i = 0; i < owners.length; ++i) { + balances[i] = _balanceOf[owners[i]][ids[i]]; + } + } + } + + /// @notice Sets the operator for the sender address. + function setApprovalForAll(address operator, bool approved) public virtual { + isApprovedForAll[msg.sender][operator] = approved; + } + + /// @notice Checks if interface of identifier `id` is supported. + /// @param id The ERC-165 interface identifier. + /// @return True if interface id `id` is supported, False otherwise. + function supportsInterface(bytes4 id) public pure virtual returns (bool) { + return + id == _ERC165_INTERFACE_ID || + id == _ERC1155_INTERFACE_ID; + } + + /// @notice Mints token of id `id` to address `to`. + /// @param to Address receiving the minted NFT. + /// @param id The id of the token type being minted. + function _mint(address to, uint256 id) internal virtual { + unchecked { + ++_balanceOf[to][id]; + } + + emit TransferSingle(msg.sender, address(0), to, id, 1); + + if ( + to.code.length != 0 && + IERC1155Receiver(to).onERC1155Received( + msg.sender, + address(0), + id, + 1, + "" + ) != + IERC1155Receiver.onERC1155Received.selector + ) { + revert SafeTransferUnsupported(); + } else if (to == address(0)) { + revert ReceiverInvalid(); + } + } + +} + diff --git a/assets/eip-5700/erc1155/ERC1155Bindable.sol b/assets/eip-5700/erc1155/ERC1155Bindable.sol new file mode 100644 index 00000000000000..d7615a2695530f --- /dev/null +++ b/assets/eip-5700/erc1155/ERC1155Bindable.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC1155Receiver} from "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol"; + +import {ERC1155} from "./ERC1155.sol"; +import {IERC1155Bindable} from "../interfaces/IERC1155Bindable.sol"; +import {IERC1155Binder} from "../interfaces/IERC1155Binder.sol"; + +/// @title ERC-1155 Bindable Reference Implementation. +/// @dev Only supports the "delegated" binding mode. +contract ERC1155Bindable is ERC1155, IERC1155Bindable { + + /// @notice Tracks the bound balance of an asset for a specific token type. + mapping(address => mapping(uint256 => mapping(uint256 => uint256))) public boundBalanceOf; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC1155_BINDER_INTERFACE_ID = 0x2ac2d2bc; + bytes4 private constant _ERC1155_BINDABLE_INTERFACE_ID = 0xd92c3ff0; + + /// @inheritdoc IERC1155Bindable + function boundBalanceOfBatch( + address bindAddress, + uint256[] calldata bindIds, + uint256[] calldata tokenIds + ) public view returns (uint256[] memory balances) { + if (bindIds.length != tokenIds.length) { + revert ArityMismatch(); + } + + balances = new uint256[](bindIds.length); + + unchecked { + for (uint256 i = 0; i < bindIds.length; ++i) { + balances[i] = boundBalanceOf[bindAddress][bindIds[i]][tokenIds[i]]; + } + } + } + + /// @inheritdoc IERC1155Bindable + function bind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) public { + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) { + revert SenderUnauthorized(); + } + + IERC1155Binder binder = IERC1155Binder(bindAddress); + if (to != bindAddress) { + revert BinderInvalid(); + } + + boundBalanceOf[bindAddress][bindId][tokenId] += amount; + _balanceOf[from][tokenId] -= amount; + _balanceOf[to][tokenId] += amount; + + emit Bind(msg.sender, from, bindAddress, tokenId, amount, bindId, bindAddress); + emit TransferSingle(msg.sender, from, bindAddress, tokenId, amount); + + if ( + binder.onERC1155Bind(msg.sender, from, to, tokenId, amount, bindId, data) + != + IERC1155Binder.onERC1155Bind.selector + ) { + revert BindInvalid(); + } + + } + + /// @inheritdoc IERC1155Bindable + function batchBind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) public { + if (msg.sender != from && !isApprovedForAll[from][msg.sender]) { + revert SenderUnauthorized(); + } + + IERC1155Binder binder = IERC1155Binder(bindAddress); + if (to != bindAddress) { + revert BinderInvalid(); + } + + if (tokenIds.length != amounts.length || tokenIds.length != bindIds.length) { + revert ArityMismatch(); + } + + for (uint256 i = 0; i < tokenIds.length; i++) { + + boundBalanceOf[bindAddress][bindIds[i]][tokenIds[i]] += amounts[i]; + _balanceOf[from][tokenIds[i]] -= amounts[i]; + _balanceOf[to][tokenIds[i]] += amounts[i]; + } + + emit BindBatch(msg.sender, from, bindAddress, tokenIds, amounts, bindIds, bindAddress); + emit TransferBatch(msg.sender, from, bindAddress, tokenIds, amounts); + + if ( + binder.onERC1155BatchBind(msg.sender, from, to, tokenIds, amounts, bindIds, data) + != + IERC1155Binder.onERC1155Bind.selector + ) { + revert BindInvalid(); + } + + } + + /// @inheritdoc IERC1155Bindable + function unbind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) public { + IERC1155Binder binder = IERC1155Binder(bindAddress); + if ( + binder.ownerOf(bindId) != from + ) { + revert BinderInvalid(); + } + + if ( + msg.sender != from && + !binder.isApprovedForAll(from, msg.sender) + ) { + revert SenderUnauthorized(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + _balanceOf[to][tokenId] += amount; + _balanceOf[from][tokenId] -= amount; + boundBalanceOf[bindAddress][bindId][tokenId] -= amount; + + emit Bind(msg.sender, bindAddress, to, tokenId, amount, bindId, bindAddress); + emit TransferSingle(msg.sender, bindAddress, to, tokenId, amount); + + if ( + binder.onERC1155Unbind(msg.sender, from, to, tokenId, amount, bindId, data) + != + IERC1155Binder.onERC1155Unbind.selector + ) { + revert BindInvalid(); + } + + if ( + to.code.length != 0 && + IERC1155Receiver(to).onERC1155Received(msg.sender, from, amount, tokenId, "") + != + IERC1155Receiver.onERC1155Received.selector + ) { + revert SafeTransferUnsupported(); + } + + } + + /// @inheritdoc IERC1155Bindable + function batchUnbind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) public { + IERC1155Binder binder = IERC1155Binder(bindAddress); + + if ( + msg.sender != from && + !binder.isApprovedForAll(from, msg.sender) + ) { + revert SenderUnauthorized(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + if (tokenIds.length != amounts.length || tokenIds.length != bindIds.length) { + revert ArityMismatch(); + } + + for (uint256 i = 0; i < tokenIds.length; i++) { + + if (binder.ownerOf(bindIds[i]) != from) { + revert BinderInvalid(); + } + + _balanceOf[to][tokenIds[i]] += amounts[i]; + _balanceOf[from][tokenIds[i]] -= amounts[i]; + boundBalanceOf[bindAddress][bindIds[i]][tokenIds[i]] -= amounts[i]; + } + + emit UnbindBatch(msg.sender, from, bindAddress, tokenIds, amounts, bindIds, bindAddress); + emit TransferBatch(msg.sender, from, bindAddress, tokenIds, amounts); + + if ( + binder.onERC1155BatchUnbind(msg.sender, from, to, tokenIds, amounts, bindIds, data) + != + IERC1155Binder.onERC1155BatchUnbind.selector + ) { + revert BindInvalid(); + } + + if ( + to.code.length != 0 && + IERC1155Receiver(to).onERC1155BatchReceived(msg.sender, from, tokenIds, amounts, "") + != + IERC1155Receiver.onERC1155BatchReceived.selector + ) { + revert SafeTransferUnsupported(); + } + } + + + function supportsInterface(bytes4 id) public pure override(ERC1155, IERC165) returns (bool) { + return super.supportsInterface(id) || id == _ERC1155_BINDABLE_INTERFACE_ID; + } + +} diff --git a/assets/eip-5700/erc1155/ERC1155Binder.sol b/assets/eip-5700/erc1155/ERC1155Binder.sol new file mode 100644 index 00000000000000..2ac1dcb5762d3e --- /dev/null +++ b/assets/eip-5700/erc1155/ERC1155Binder.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import {IERC1155Bindable} from "../interfaces/IERC1155Bindable.sol"; +import {IERC1155Binder} from "../interfaces/IERC1155Binder.sol"; + +/// @title ERC-1155 Binder Reference Implementation +contract ERC1155Binder is IERC1155Binder { + + struct Bindable { + address tokenAddress; + uint256 tokenId; + } + + /// @notice Checks for an owner if an address is an authorized operator. + mapping(address => mapping(address => bool)) public _isApprovedForAll; + + /// @notice Tracks ownership of bound assets. + mapping(uint256 => address) _ownerOf; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC1155_BINDER_INTERFACE_ID = 0x2ac2d2bc; + bytes4 private constant _ERC1155_BINDABLE_INTERFACE_ID = 0xd92c3ff0; + + /// @inheritdoc IERC1155Binder + function isApprovedForAll(address owner, address operator) external view override returns (bool) { + return _isApprovedForAll[owner][operator]; + } + + /// @inheritdoc IERC1155Binder + function ownerOf(uint256 id) public view returns (address) { + return _ownerOf[id]; + } + + /// @inheritdoc IERC1155Binder + function onERC1155Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) public returns (bytes4) { + return IERC1155Binder.onERC1155Bind.selector; + } + + /// @inheritdoc IERC1155Binder + function onERC1155BatchBind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) public returns (bytes4) { + return IERC1155Binder.onERC1155BatchBind.selector; + } + + /// @inheritdoc IERC1155Binder + function onERC1155Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) public returns (bytes4) { + return IERC1155Binder.onERC1155Unbind.selector; + } + + /// @inheritdoc IERC1155Binder + function onERC1155BatchUnbind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) public returns (bytes4) { + return IERC1155Binder.onERC1155BatchUnbind.selector; + } + + function supportsInterface(bytes4 id) external pure returns (bool) { + return id == _ERC165_INTERFACE_ID || id == _ERC1155_BINDER_INTERFACE_ID; + } + + /// @notice Mints a new asset identified by `id` to address `to`. + function _mint(address to, uint256 id) internal { + if (to == address(0)) { + revert ReceiverInvalid(); + } + + if (_ownerOf[id] != address(0)) { + revert AssetAlreadyMinted(); + } + + _ownerOf[id] = to; + } + +} diff --git a/assets/eip-5700/erc721/ERC721.sol b/assets/eip-5700/erc721/ERC721.sol new file mode 100644 index 00000000000000..c3f50d98bbdf00 --- /dev/null +++ b/assets/eip-5700/erc721/ERC721.sol @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +import {IERC721Errors} from "../interfaces/IERC721Errors.sol"; + +/// @title Reference Minimal ERC-721 Contract +contract ERC721 is IERC721, IERC721Errors { + + /// @notice The total number of NFTs in circulation. + uint256 public totalSupply; + + /// @notice Gets the approved address for an NFT. + /// @dev This implementation does not throw for zero-address queries. + mapping(uint256 => address) public getApproved; + + /// @notice Gets the number of NFTs owned by an address. + mapping(address => uint256) internal _balanceOf; + + /// @dev Tracks the assigned owner of an address. + mapping(uint256 => address) internal _ownerOf; + + /// @dev Checks for an owner if an address is an authorized operator. + mapping(address => mapping(address => bool)) internal _operatorApprovals; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC721_INTERFACE_ID = 0x80ac58cd; + + /// @notice Gets the assigned owner for token `id`. + /// @param id The id of the NFT being queried. + /// @return The address of the owner of the NFT of id `id`. + function ownerOf(uint256 id) external view virtual returns (address) { + return _ownerOf[id]; + } + + /// @notice Gets number of NFTs owned by address `owner`. + /// @param owner The address whose balance is being queried. + /// @return The number of NFTs owned by address `owner`. + function balanceOf(address owner) external view virtual returns (uint256) { + return _balanceOf[owner]; + } + + /// @notice Sets approved address of NFT of id `id` to address `approved`. + /// @param approved The new approved address for the NFT. + /// @param id The id of the NFT to approve. + function approve(address approved, uint256 id) external virtual { + address owner = _ownerOf[id]; + + if (msg.sender != owner && !_operatorApprovals[owner][msg.sender]) { + revert SenderUnauthorized(); + } + + getApproved[id] = approved; + emit Approval(owner, approved, id); + } + + /// @notice Checks if `operator` is an authorized operator for `owner`. + /// @param owner The address of the owner. + /// @param operator The address of the owner's operator. + /// @return True if `operator` is approved operator of `owner`, else False. + function isApprovedForAll(address owner, address operator) + external + view + virtual returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + /// @notice Sets the operator for `msg.sender` to `operator`. + /// @param operator The operator address that will manage the sender's NFTs. + /// @param approved Whether operator is allowed to operate sender's NFTs. + function setApprovalForAll(address operator, bool approved) external virtual { + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /// @notice Checks if interface of identifier `id` is supported. + /// @param id The ERC-165 interface identifier. + /// @return True if interface id `id` is supported, false otherwise. + function supportsInterface(bytes4 id) public pure virtual returns (bool) { + return + id == _ERC165_INTERFACE_ID || + id == _ERC721_INTERFACE_ID; + } + + /// @notice Transfers NFT of id `id` from address `from` to address `to`, + /// with safety checks ensuring `to` is capable of receiving the NFT. + /// @dev Safety checks are only performed if `to` is a smart contract. + /// @param from The existing owner address of the NFT to be transferred. + /// @param to The new owner address of the NFT being transferred. + /// @param id The id of the NFT being transferred. + /// @param data Additional transfer data to pass to the receiving contract. + function safeTransferFrom( + address from, + address to, + uint256 id, + bytes memory data + ) public virtual { + transferFrom(from, to, id); + + if ( + to.code.length != 0 && + IERC721Receiver(to).onERC721Received(msg.sender, from, id, data) + != + IERC721Receiver.onERC721Received.selector + ) { + revert SafeTransferUnsupported(); + } + } + + /// @notice Transfers NFT of id `id` from address `from` to address `to`, + /// with safety checks ensuring `to` is capable of receiving the NFT. + /// @dev Safety checks are only performed if `to` is a smart contract. + /// @param from The existing owner address of the NFT to be transferred. + /// @param to The new owner address of the NFT being transferred. + /// @param id The id of the NFT being transferred. + function safeTransferFrom( + address from, + address to, + uint256 id + ) public virtual { + transferFrom(from, to, id); + + if ( + to.code.length != 0 && + IERC721Receiver(to).onERC721Received(msg.sender, from, id, "") + != + IERC721Receiver.onERC721Received.selector + ) { + revert SafeTransferUnsupported(); + } + } + + /// @notice Transfers NFT of id `id` from address `from` to address `to`, + /// without performing any safety checks. + /// @dev Existence of an NFT is inferred by having a non-zero owner address. + /// Transfers clear owner approvals, but `Approval` events are omitted. + /// @param from The existing owner address of the NFT being transferred. + /// @param to The new owner address of the NFT being transferred. + /// @param id The id of the NFT being transferred. + function transferFrom( + address from, + address to, + uint256 id + ) public virtual { + if (from != _ownerOf[id]) { + revert OwnerInvalid(); + } + + if ( + msg.sender != from && + msg.sender != getApproved[id] && + !_operatorApprovals[from][msg.sender] + ) { + revert SenderUnauthorized(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + _beforeTokenTransfer(from, to, id); + + delete getApproved[id]; + + unchecked { + _balanceOf[from]--; + _balanceOf[to]++; + } + + _ownerOf[id] = to; + emit Transfer(from, to, id); + } + + /// @dev Mints NFT of id `id` to address `to`. To save gas, it is assumed + /// that `maxSupply` < `type(uint256).max` (ex. for tabs, cap is very low). + /// @param to Address receiving the minted NFT. + /// @param id Identifier of the NFT being minted. + /// @return The id of the minted NFT. + function _mint(address to, uint256 id) internal virtual returns (uint256) { + if (to == address(0)) { + revert ReceiverInvalid(); + } + if (_ownerOf[id] != address(0)) { + revert TokenAlreadyMinted(); + } + + _beforeTokenTransfer(address(0), to, id); + + unchecked { + totalSupply++; + _balanceOf[to]++; + } + + _ownerOf[id] = to; + emit Transfer(address(0), to, id); + return id; + } + + /// @dev Burns NFT of id `id`, removing it from existence. + /// @param id Identifier of the NFT being burned + function _burn(uint256 id) internal virtual { + address owner = _ownerOf[id]; + + if (owner == address(0)) { + revert TokenNonExistent(); + } + + _beforeTokenTransfer(owner, address(0), id); + + unchecked { + totalSupply--; + _balanceOf[owner]--; + } + + delete _ownerOf[id]; + emit Transfer(owner, address(0), id); + } + + /// @notice Pre-transfer hook for embedding additional transfer behavior. + /// @param from The address of the existing owner of the NFT. + /// @param to The address of the new owner of the NFT. + /// @param id The id of the NFT being transferred. + function _beforeTokenTransfer(address from, address to, uint256 id) + internal + virtual + {} + +} diff --git a/assets/eip-5700/erc721/ERC721Bindable.sol b/assets/eip-5700/erc721/ERC721Bindable.sol new file mode 100644 index 00000000000000..d772721e6cc5aa --- /dev/null +++ b/assets/eip-5700/erc721/ERC721Bindable.sol @@ -0,0 +1,210 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; + +import {ERC721} from "./ERC721.sol"; +import {IERC721Bindable} from "../interfaces/IERC721Bindable.sol"; +import {IERC721Binder} from "../interfaces/IERC721Binder.sol"; + +/// @title ERC-721 Bindable Reference Implementation. +/// @dev Supports both "legacy" and "delegated" binding modes. +contract ERC721Bindable is ERC721, IERC721Bindable { + + /// @notice Encapsulates a bound asset contract address and identifier. + struct Binder { + address bindAddress; + uint256 bindId; + } + + /// @notice Tracks the bound balance of a specific asset. + mapping(address => mapping(uint256 => uint256)) public boundBalanceOf; + + /// @notice Tracks bound assets of an NFT. + mapping(uint256 => Binder) internal _bound; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC721_BINDER_INTERFACE_ID = 0x2ac2d2bc; + bytes4 private constant _ERC721_BINDABLE_INTERFACE_ID = 0xd92c3ff0; + + /// @inheritdoc IERC721Bindable + function binderOf(uint256 tokenId) public returns (address, uint256) { + Binder memory bound = _bound[tokenId]; + return (bound.bindAddress, bound.bindId); + } + + /// @inheritdoc IERC721Bindable + function bind( + address from, + address to, + uint256 tokenId, + uint256 bindId, + address bindAddress, + bytes calldata data + ) public { + if (_bound[tokenId].bindAddress != address(0)) { + revert BindExistent(); + } + + if (from != _ownerOf[tokenId]) { + revert OwnerInvalid(); + } + + IERC721Binder binder = IERC721Binder(bindAddress); + address assetOwner = binder.ownerOf(bindId); + if (to != assetOwner && to != bindAddress) { + revert BinderInvalid(); + } + + if ( + msg.sender != from && + msg.sender != getApproved[tokenId] && + !_operatorApprovals[from][msg.sender] + ) { + revert SenderUnauthorized(); + } + + delete getApproved[tokenId]; + + unchecked { + _balanceOf[from]--; + _balanceOf[bindAddress]++; + _balanceOf[assetOwner]++; + boundBalanceOf[bindAddress][bindId]++; + } + + _ownerOf[tokenId] = to; + _bound[tokenId] = Binder(bindAddress, bindId); + + emit Bind(msg.sender, from, to, tokenId, bindId, bindAddress); + emit Transfer(from, to, tokenId); + + if ( + binder.onERC721Bind(msg.sender, from, to, tokenId, bindId, "") + != + IERC721Binder.onERC721Bind.selector + ) { + revert BindInvalid(); + } + + } + + /// @inheritdoc IERC721Bindable + function unbind( + address from, + address to, + uint256 tokenId, + uint256 bindId, + address bindAddress, + bytes calldata data + ) public { + Binder memory bound = _bound[tokenId]; + if (bound.bindAddress != address(0)) { + revert BindNonexistent(); + } + + IERC721Binder binder = IERC721Binder(bindAddress); + if ( + bound.bindAddress != bindAddress || + bound.bindId != bindId || + binder.ownerOf(bindId) != from + ) { + revert BinderInvalid(); + } + + if ( + msg.sender != from && + !binder.isApprovedForAll(from, msg.sender) + ) { + revert SenderUnauthorized(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + address delegatedOwner = _ownerOf[tokenId]; + + delete getApproved[tokenId]; + + unchecked { + _balanceOf[to]++; + _balanceOf[from]--; + _balanceOf[bindAddress]--; + boundBalanceOf[bindAddress][bindId]--; + } + + _ownerOf[tokenId] = to; + delete _bound[tokenId]; + + emit Bind(msg.sender, from, to, tokenId, bindId, bindAddress); + emit Transfer(delegatedOwner, to, tokenId); + + if ( + binder.onERC721Unbind(msg.sender, from, to, tokenId, bindId, "") + != + IERC721Binder.onERC721Unbind.selector + ) { + revert BindInvalid(); + } + + if ( + to.code.length != 0 && + IERC721Receiver(to).onERC721Received(msg.sender, delegatedOwner, tokenId, "") + != + IERC721Receiver.onERC721Received.selector + ) { + revert SafeTransferUnsupported(); + } + + } + + /// @inheritdoc IERC721 + function transferFrom( + address from, + address to, + uint256 tokenId + ) public override(IERC721, ERC721) { + + address bindAddress = _bound[tokenId].bindAddress; + uint256 bindId = _bound[tokenId].bindId; + + if (bindAddress == address(0)) { + return super.transferFrom(from, to, tokenId); + } + + if (msg.sender != bindAddress) { + revert BindExistent(); + } + + IERC721Binder binder = IERC721Binder(bindAddress); + + if ( + binder.ownerOf(bindId) != from + ) { + revert BinderInvalid(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + delete getApproved[tokenId]; + + uint256 bindBal = boundBalanceOf[bindAddress][bindId]; + unchecked { + _balanceOf[from] -= bindBal; + _balanceOf[to] += bindBal; + } + + emit Transfer(from, to, tokenId); + } + + function supportsInterface(bytes4 id) public pure override(ERC721, IERC165) returns (bool) { + return super.supportsInterface(id) || id == _ERC721_BINDABLE_INTERFACE_ID; + } + +} diff --git a/assets/eip-5700/erc721/ERC721Binder.sol b/assets/eip-5700/erc721/ERC721Binder.sol new file mode 100644 index 00000000000000..e8f939aae7b15f --- /dev/null +++ b/assets/eip-5700/erc721/ERC721Binder.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {ERC721} from "./ERC721.sol"; +import {IERC721Bindable} from "../interfaces/IERC721Bindable.sol"; +import {IERC721Binder} from "../interfaces/IERC721Binder.sol"; + +/// @title ERC-721 Binder Reference Implementation +contract ERC721Binder is IERC721Binder { + + struct Bindable { + address tokenAddress; + uint256 tokenId; + } + + /// @notice Checks for an owner if an address is an authorized operator. + mapping(address => mapping(address => bool)) public _isApprovedForAll; + + /// @notice Tracks ownership of bound assets. + mapping(uint256 => address) _ownerOf; + + /// @notice Maps an asset to a list of all bound bindables. + mapping(uint256 => Bindable[]) _boundTokens; + + /// @notice Maps a token address and identifier to the bound tokens index. + mapping(address => mapping(uint256 => uint256)) _boundIndexes; + + /// @dev EIP-165 identifiers for all supported interfaces. + bytes4 private constant _ERC165_INTERFACE_ID = 0x01ffc9a7; + bytes4 private constant _ERC721_BINDER_INTERFACE_ID = 0x2ac2d2bc; + bytes4 private constant _ERC721_BINDABLE_INTERFACE_ID = 0xd92c3ff0; + + /// @inheritdoc IERC721Binder + function isApprovedForAll(address owner, address operator) external view override returns (bool) { + return _isApprovedForAll[owner][operator]; + } + + /// @inheritdoc IERC721Binder + function ownerOf(uint256 id) public view returns (address) { + return _ownerOf[id]; + } + + /// @inheritdoc IERC721Binder + function onERC721Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) public returns (bytes4) { + if (_ownerOf[bindId] != to) { + revert OwnerInvalid(); + } + + if (_boundIndexes[msg.sender][tokenId] != 0) { + revert BindExistent(); + } + + if (!IERC721Bindable(msg.sender).supportsInterface(_ERC721_BINDABLE_INTERFACE_ID)) { + revert BindInvalid(); + } + + _boundIndexes[msg.sender][tokenId] = _boundTokens[bindId].length; + _boundTokens[bindId].push(Bindable(msg.sender, tokenId)); + + return IERC721Binder.onERC721Bind.selector; + } + + /// @inheritdoc IERC721Binder + function onERC721Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) public returns (bytes4) { + if (_ownerOf[bindId] != from) { + revert OwnerInvalid(); + } + + if (_boundIndexes[msg.sender][tokenId] == 0) { + revert BindNonexistent(); + } + + uint256 boundLastIndex = _boundTokens[bindId].length - 1; + uint256 boundIndex = _boundIndexes[msg.sender][tokenId]; + + if (boundIndex != boundLastIndex) { + Bindable memory bindable = _boundTokens[bindId][boundLastIndex]; + _boundTokens[bindId][boundIndex] = bindable; + _boundIndexes[bindable.tokenAddress][bindable.tokenId] = boundIndex; + } + + delete _boundIndexes[msg.sender][tokenId]; + delete _boundTokens[bindId][boundLastIndex]; + + return IERC721Binder.onERC721Unbind.selector; + } + + /// @notice Transfers an asset from address `from` to address `to`. + function transfer( + address from, + address to, + uint256 bindId + ) public { + if (msg.sender != from && !_isApprovedForAll[from][msg.sender]) { + revert SenderUnauthorized(); + } + + if (from != _ownerOf[bindId]) { + revert OwnerInvalid(); + } + + if (to == address(0)) { + revert ReceiverInvalid(); + } + + _ownerOf[bindId] = to; + + Bindable[] memory bindables = _boundTokens[bindId]; + for (uint256 i = 0; i < bindables.length; ++i) { + IERC721Bindable(bindables[i].tokenAddress).transferFrom(from, to, bindables[i].tokenId); + } + + } + + function supportsInterface(bytes4 id) external pure returns (bool) { + return id == _ERC165_INTERFACE_ID || id == _ERC721_BINDER_INTERFACE_ID; + } + +} diff --git a/assets/eip-5700/interfaces/IERC1155Bindable.sol b/assets/eip-5700/interfaces/IERC1155Bindable.sol new file mode 100644 index 00000000000000..c4d491a6e981af --- /dev/null +++ b/assets/eip-5700/interfaces/IERC1155Bindable.sol @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC1155} from "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; + +import {IERC1155BindableErrors} from "./IERC1155BindableErrors.sol"; + +/// @title ERC-1155 Bindable Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-5656 +/// Note: the ERC-165 identifier for this interface is 0xd0d555c6. +interface IERC1155Bindable is IERC1155, IERC1155BindableErrors { + + /// @notice The `Bind` event MUST emit when token ownership is delegated + /// through an asset and when minting tokens bound to an existing asset. + /// @dev When minting bound tokens, `from` MUST be set to the zero address. + /// @param operator The address calling the bind (SHOULD be `msg.sender`). + /// @param from The address which owns the unbound token(s). + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the token type being bound. + /// @param amount The number of tokens of type `tokenId` being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + event Bind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `BindBatch` event MUST emit when token ownership of + /// different token types are delegated through different assets at once + /// and when minting multiple token types bound to existing assets at once. + /// @dev When minting bound tokens, `from` MUST be set to the zero address. + /// @param operator The address calling the bind (SHOULD be `msg.sender`). + /// @param from The address which owns the unbound token(s). + /// @param to The address which owns the asset being bound to. + /// @param tokenIds The identifiers of the token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param bindAddress The contract address handling asset ownership. + event BindBatch( + address indexed operator, + address indexed from, + address to, + uint256[] tokenIds, + uint256[] amounts, + uint256[] bindIds, + address indexed bindAddress + ); + + /// @notice The `Unbind` event MUST emit when asset-delegated token + /// ownership is revoked and when burning tokens bound to existing assets. + /// @dev When burning bound tokens, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind (SHOULD be `msg.sender`). + /// @param from The address which owns the asset the token(s) are bound to. + /// @param to The address which will own the token(s) once unbound. + /// @param tokenId The identifier of the token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event Unbind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `UnbindBatch` event MUST emit when asset-delegated token + /// ownership is revoked for multiple token types at once and when burning + /// multiple token types bound to existing assets at once. + /// @dev When burning bound tokens, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind (SHOULD be `msg.sender`). + /// @param from The address which owns the asset the token(s) are bound to. + /// @param to The address which will own the token(s) once unbound. + /// @param tokenIds The identifiers of the token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifier of the assets being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event UnbindBatch( + address indexed operator, + address indexed from, + address to, + uint256[] tokenIds, + uint256[] amounts, + uint256[] bindIds, + address indexed bindAddress + ); + + /// @notice Delegates ownership of `amount` tokens of type `tokenId` from + /// address `from` through asset `bindId` owned by address `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// for `from`. The function also MUST throw if `from` owns fewer than + /// `amount` unbound tokens, or if `to` is not the asset owner. After + /// delegation of ownership, the function MUST check if `bindAddress` is a + /// valid contract (code size > 0), and if so, call `onERC1155Bind` on the + /// contract, throwing if the wrong identifier is returned (see "Binding + /// Rules") or if the contract is invalid. On bind completion, the function + /// MUST emit both `Bind` and IERC-1155 `TransferSingle` events to reflect + /// delegated ownership change. + /// @param from The address which owns the unbound token(s). + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the token type being bound. + /// @param amount The number of tokens of type `tokenId` being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC1155Bind` hook. + function bind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Delegates ownership of `amounts` tokens of types `tokenIds` from + /// address `from` through assets `bindIds` owned by address `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// for `from`. The function also MUST throw if length of `amounts` is not + /// the same as `tokenIds` or `bindIds`, if any unbound balances of + /// `tokenIds` for `from` is less than that of `amounts`, or if `to` is not + /// the asset owner. After delegating ownership, the function MUST check if + /// `bindAddress` is a valid contract (code size > 0), and if so, call + /// `onERC1155BatchBind` on the contract, throwing if the wrong identifier + /// is returned (see "Binding Rules") or if the contract is invalid. On + /// bind completion, the function MUST emit both `BindBatch` and IERC-1155 + /// `TransferBatch` events to reflect delegated ownership changes. + /// @param from The address which owns the unbound tokens. + /// @param to The address which owns the assets being bound to. + /// @param tokenIds The identifiers of the token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC1155BatchBind` hook. + function batchBind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Revokes delegated ownership of `amount` tokens of type `tokenId` + /// owned by `from` bound to `bindId`, binding direct ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// or owner of the delegated asset `tokenId` is bound to. It also MUST + /// throw if `from` owns fewer than `amount` bound tokens, or if `to` is + /// the zero address. Once delegated ownership is revoked, the function + /// MUST check if `bindAddress` is a valid contract (code size > 0), and if + /// so, call `onERC1155Unbind` on the contract, throwing if the wrong + /// identifier is returned (see "Binding Rules") or if the contract is + /// invalid. The function also MUST check if `to` is a contract, and if so, + /// call on it `onERC1155Received`, throwing if the wrong identifier is + /// returned. On unbind completion, the function MUST emit both `Unbind` + /// and IERC-1155 `TransferSingle` events to reflect delegated ownership change. + /// @param from The address which owns the asset the token(s) are bound to. + /// @param to The address which will own the tokens once unbound. + /// @param tokenId The identifier of the token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC1155Unbind` hook. + function unbind( + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Revokes delegated ownership of `amounts` tokens of `tokenIds` + /// bound to assets `bindIds`, binding direct ownership to `to`. + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// or owner of all delegated assets `tokenIds` are bound to. It also MUST + /// throw if the length of `amounts` is not the same as `tokenIds` or + /// `bindIds`, if any bound balances of `tokenId` for `from` is less than + /// that of `amounts`, or if `to` is the zero address. Once delegated + /// ownership is revoked, the function MUST check if `bindAddress` is a + /// valid contract (code size > 0), and if so, call onERC1155BatchUnbind` + /// on it, throwing if a wrong identifier is returned (see "Binding Rules") + /// or if the contract is invalid. The function also MUST check if `to` is + /// a valid contract, and if so, call `onERC1155BatchReceived`, throwing if + /// the wrong identifier is returned. On unbind completion, the function + /// MUST emit the `BatchUnbind` and IERC-1155 `TransferBatch` events to + /// reflect delegated ownership changes. + /// @param from The address which owns the asset the tokens are bound to. + /// @param to The address which will own the tokens once unbound. + /// @param tokenIds The identifiers of the token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifier of the assets being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC1155BatchUnbind` hook. + function batchUnbind( + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Gets the balance of bound tokens of type `tokenId` bound to the + /// asset `bindId` at address `bindAddress`. + /// @param bindId The identifier of the bound asset. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param tokenId The identifier of the bound token type being counted. + /// @return The total number of NFTs bound to the asset. + function boundBalanceOf( + address bindAddress, + uint256 bindId, + uint256 tokenId + ) external returns (uint256); + + /// @notice Gets the balance of bound tokens for multiple token types given + /// by `tokenIds` bound to assets `bindIds` at address `bindAddress`. + /// @notice Retrieves bound balances of multiple asset / token type pairs. + /// @param bindIds List of bound asset identifiers. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param tokenIds The identifiers of the token type being counted. + /// @return balances The bound balances for each asset / token type pair. + function boundBalanceOfBatch( + address bindAddress, + uint256[] calldata bindIds, + uint256[] calldata tokenIds + ) external returns (uint256[] memory balances); + +} diff --git a/assets/eip-5700/interfaces/IERC1155BindableErrors.sol b/assets/eip-5700/interfaces/IERC1155BindableErrors.sol new file mode 100644 index 00000000000000..4b6aefa89cd352 --- /dev/null +++ b/assets/eip-5700/interfaces/IERC1155BindableErrors.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-1155 Bindable Errors Interface +interface IERC1155BindableErrors { + + /// @notice Bind is not valid. + error BindInvalid(); + + /// @notice Bound asset or bound asset owner is not valid. + error BinderInvalid(); + +} diff --git a/assets/eip-5700/interfaces/IERC1155Binder.sol b/assets/eip-5700/interfaces/IERC1155Binder.sol new file mode 100644 index 00000000000000..4466ae7e372f0a --- /dev/null +++ b/assets/eip-5700/interfaces/IERC1155Binder.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {IERC1155BinderErrors} from "./IERC1155BinderErrors.sol"; + +/// @dev Note: the ERC-165 identifier for this interface is 0x6fc97e78. +interface IERC1155Binder is IERC165, IERC1155BinderErrors { + + /// @notice Handles binding of an IERC1155Bindable-compliant token type. + /// @dev An IERC1155Bindable-compliant smart contract MUST call this + /// function at the end of a `bind` after delegating ownership to the asset + /// owner. The function MUST revert if `to` is not the asset owner of + /// `bindId`, or if `bindId` is not a valid asset. The function MUST revert + /// if it rejects the bind. If accepting the bind, the function MUST return + /// `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding token is `msg.sender`. + /// @param operator The address responsible for binding. + /// @param from The address which owns the unbound tokens. + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the token type being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256,uint256,uint256,bytes)"))` + function onERC1155Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles binding of multiple IERC1155Bindable-compliant tokens + /// `tokenIds` to multiple assets `bindIds`. + /// @dev An IERC1155Bindable-compliant smart contract MUST call this + /// function at the end of a `batchBind` after delegating ownership of + /// multiple token types to the asset owner. The function MUST revert if + /// `to` is not the asset owner of `bindId`, or if `bindId` is not a valid + /// asset. The function MUST revert if it rejects the binds. If accepting + /// the binds, the function MUST return `bytes4(keccak256("onERC1155BatchBind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding token is `msg.sender`. + /// @param operator The address responsible for performing the binds. + /// @param from The address which owns the unbound tokens. + /// @param to The address which owns the assets being bound to. + /// @param tokenIds The list of token types being bound. + /// @param amounts The number of tokens for each token type being bound. + /// @param bindIds The identifiers of the assets being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Bind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + function onERC1155BatchBind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles unbinding of an IERC1155Bindable-compliant token type. + /// @dev An IERC1155Bindable-compliant contract MUST call this function at + /// the end of an `unbind` after revoking delegated asset ownership. The + /// function MUST revert if `from` is not the asset owner of `bindId`, + /// or if `bindId` is not a valid asset. The function MUST revert if it + /// rejects the unbind. If accepting the unbind, the function MUST return + /// `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding token is `msg.sender`. + /// @param operator The address responsible for performing the unbind. + /// @param from The address which owns the asset the token type is bound to. + /// @param to The address which will own the tokens once unbound. + /// @param tokenId The token type being unbound. + /// @param amount The number of tokens of type `tokenId` being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256,uint256,uint256,bytes)"))` + function onERC1155Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 amount, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles unbinding of multiple IERC1155Bindable-compliant token types. + /// @dev An IERC1155Bindable-compliant contract MUST call this function at + /// the end of an `batchUnbind` after revoking delegated asset ownership. + /// The function MUST revert if `from` is not the asset owner of `bindId`, + /// or if `bindId` is not a valid asset. The function MUST revert if it + /// rejects the unbinds. If accepting the unbinds, the function MUST return + /// `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding token is `msg.sender`. + /// @param operator The address responsible for performing the unbinds. + /// @param from The address which owns the assets being unbound from. + /// @param to The address which will own the tokens once unbound. + /// @param tokenIds The list of token types being unbound. + /// @param amounts The number of tokens for each token type being unbound. + /// @param bindIds The identifiers of the assets being unbound from. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC1155Unbind(address,address,address,uint256[],uint256[],uint256[],bytes)"))` + function onERC1155BatchUnbind( + address operator, + address from, + address to, + uint256[] calldata tokenIds, + uint256[] calldata amounts, + uint256[] calldata bindIds, + bytes calldata data + ) external returns (bytes4); + + /// @notice Gets the owner address of the asset represented by id `bindId`. + /// @param bindId The identifier of the asset whose owner is being queried. + /// @return The address of the owner of the asset. + function ownerOf(uint256 bindId) external view returns (address); + + /// @notice Checks if an operator can act on behalf of an asset owner. + /// @param owner The address that owns an asset. + /// @param operator The address that acts on behalf of owner `owner`. + /// @return True if `operator` can act on behalf of `owner`, else False. + function isApprovedForAll(address owner, address operator) external view returns (bool); + +} diff --git a/assets/eip-5700/interfaces/IERC1155BinderErrors.sol b/assets/eip-5700/interfaces/IERC1155BinderErrors.sol new file mode 100644 index 00000000000000..9c438338910d66 --- /dev/null +++ b/assets/eip-5700/interfaces/IERC1155BinderErrors.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-1155 Binder Errors Interface +interface IERC1155BinderErrors { + + /// @notice Asset has already minted. + error AssetAlreadyMinted(); + + /// @notice Receiving address cannot be the zero address. + error ReceiverInvalid(); + +} diff --git a/assets/eip-5700/interfaces/IERC1155Errors.sol b/assets/eip-5700/interfaces/IERC1155Errors.sol new file mode 100644 index 00000000000000..d3f07129986394 --- /dev/null +++ b/assets/eip-5700/interfaces/IERC1155Errors.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-1155 Errors Interface +interface IERC1155Errors { + + /// @notice Arity mismatch between two arrays. + error ArityMismatch(); + + /// @notice Originating address does not own the NFT. + error OwnerInvalid(); + + /// @notice Receiving address cannot be the zero address. + error ReceiverInvalid(); + + /// @notice Receiving contract does not implement the ERC-1155 wallet interface. + error SafeTransferUnsupported(); + + /// @notice Sender is not NFT owner, approved address, or owner operator. + error SenderUnauthorized(); + + /// @notice Token has already minted. + error TokenAlreadyMinted(); + + /// @notice NFT does not exist. + error TokenNonExistent(); + +} diff --git a/assets/eip-5700/interfaces/IERC721Bindable.sol b/assets/eip-5700/interfaces/IERC721Bindable.sol new file mode 100644 index 00000000000000..f8ac283ec25c1c --- /dev/null +++ b/assets/eip-5700/interfaces/IERC721Bindable.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import {IERC721BindableErrors} from "./IERC721BindableErrors.sol"; + +/// @title ERC-721 Bindable Token Standard +/// @dev See https://eips.ethereum.org/EIPS/eip-5700 +/// Note: the ERC-165 identifier for this interface is 0x82a34a7d. +interface IERC721Bindable is IERC721, IERC721BindableErrors { + + /// @notice The `Bind` event MUST emit when NFT ownership is delegated + /// through an asset and when minting an NFT bound to an existing asset. + /// @dev When minting bound NFTs, `from` MUST be set to the zero address. + /// @param operator The address calling the bind (SHOULD be `msg.sender`). + /// @param from The address which owns the unbound NFT. + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + event Bind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice The `Unbind` event MUST emit when asset-delegated NFT ownership + /// is revoked, as well as when burning an NFT bound to an existing asset. + /// @dev When burning bound NFTs, `to` MUST be set to the zero address. + /// @param operator The address calling the unbind (SHOULD be `msg.sender`). + /// @param from The address which owns the asset the NFT is bound to. + /// @param to The address which will own the NFT once unbound. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + event Unbind( + address indexed operator, + address indexed from, + address to, + uint256 tokenId, + uint256 bindId, + address indexed bindAddress + ); + + /// @notice Delegates NFT ownership of NFT `tokenId` from address `from` + /// through the asset `bindId` owned by address `to`. + /// @dev The function MUST throw unless `msg.sender` is the current owner, + /// an authorized operator, or the approved address for the NFT. It also + /// MUST throw if NFT `tokenId` is already bound, if `from` is not the NFT + /// owner, or if `to` is not the asset owner. After ownership delegation, + /// the function MUST check if `bindAddress` is a valid contract (code size + /// > 0), and if so, call `onERC721Bind` on the contract, throwing if the + /// wrong identifier is returned (see "Binding Rules") or if the contract + /// is invalid. On bind completion, the function MUST emit both `Bind` and + /// IERC-721 `Transfer` events to reflect delegated ownership change. + /// @param from The address which owns the unbound NFT. + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param bindAddress The contract address handling asset ownership. + /// @param data Additional data sent with the `onERC721Bind` hook. + function bind( + address from, + address to, + uint256 tokenId, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @dev The function MUST throw unless `msg.sender` is an approved operator + /// or owner of the delegated asset of `tokenId`. It also MUST throw if NFT + /// `tokenId` is not bound, if `from` is not the asset owner, or if `to` + /// is the zero address. After ownership transition, the function MUST + /// check if `bindAddress` is a valid contract (code size > 0), and if so, + /// call `onERC721Unbind` the contract, throwing if the wrong identifier is + /// returned (see "Binding Rules") or if the contract is invalid. + /// The function also MUST check if `to` is a valid contract, and if so, + /// call `onERC721Received`, throwing if the wrong identifier is returned. + /// On unbind completion, the function MUST emit both `Unbind` and IERC-721 + /// `Transfer` events to reflect delegated ownership change. + /// @param from The address which owns the asset the NFT is bound to. + /// @param to The address which will own the NFT once unbound. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param bindAddress The contract address handling bound asset ownership. + /// @param data Additional data sent with the `onERC721Unbind` hook. + function unbind( + address from, + address to, + uint256 tokenId, + uint256 bindId, + address bindAddress, + bytes calldata data + ) external; + + /// @notice Gets the asset identifier and address which a token is bound to. + /// @param tokenId The identifier of the NFT being queried. + /// @return The bound asset identifier and contract address. + function binderOf(uint256 tokenId) external returns (address, uint256); + + /// @notice Counts NFTs bound to asset `bindId` at address `bindAddress`. + /// @param bindId The identifier of the bound asset. + /// @param bindAddress The contract address handling bound asset ownership. + /// @return The total number of NFTs bound to the asset. + function boundBalanceOf(address bindAddress, uint256 bindId) external returns (uint256); + +} diff --git a/assets/eip-5700/interfaces/IERC721BindableErrors.sol b/assets/eip-5700/interfaces/IERC721BindableErrors.sol new file mode 100644 index 00000000000000..112493270eecdf --- /dev/null +++ b/assets/eip-5700/interfaces/IERC721BindableErrors.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-721 Bindable Errors Interface +interface IERC721BindableErrors { + + /// @notice Bind already exists. + error BindExistent(); + + /// @notice Bind does not exist. + error BindNonexistent(); + + /// @notice Bind is not valid. + error BindInvalid(); + + /// @notice Bound asset or bound asset owner is not valid. + error BinderInvalid(); + +} diff --git a/assets/eip-5700/interfaces/IERC721Binder.sol b/assets/eip-5700/interfaces/IERC721Binder.sol new file mode 100644 index 00000000000000..93426e29f56fce --- /dev/null +++ b/assets/eip-5700/interfaces/IERC721Binder.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +import {IERC721BinderErrors} from "./IERC721BinderErrors.sol"; + +/// @dev Note: the ERC-165 identifier for this interface is 0x2ac2d2bc. +interface IERC721Binder is IERC165, IERC721BinderErrors { + + /// @notice Handles the binding of an IERC721Bindable-compliant NFT. + /// @dev An IERC721Bindable-compliant smart contract MUST call this function + /// at the end of a `bind` after delegating ownership to the asset owner. + /// The function MUST revert if `to` is not the asset owner of `bindId` or + /// if asset `bindId` is not a valid asset. The function MUST revert if it + /// rejects the bind. If accepting the bind, the function MUST return + /// `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the binding NFT is `msg.sender`. + /// @param operator The address responsible for initiating the bind. + /// @param from The address which owns the unbound NFT. + /// @param to The address which owns the asset being bound to. + /// @param tokenId The identifier of the NFT being bound. + /// @param bindId The identifier of the asset being bound to. + /// @param data Additional data sent along with no specified format. + /// @return `bytes4(keccak256("onERC721Bind(address,address,address,uint256,uint256,bytes)"))` + function onERC721Bind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Handles the unbinding of an IERC721Bindable-compliant NFT. + /// @dev An IERC721Bindable-compliant smart contract MUST call this function + /// at the end of an `unbind` after revoking delegated asset ownership. + /// The function MUST revert if `from` is not the asset owner of `bindId` + /// or if `bindId` is not a valid asset. The function MUST revert if it + /// rejects the unbind. If accepting the unbind, the function MUST return + /// `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))` + /// Caller MUST revert the transaction if the above value is not returned. + /// Note: The contract address of the unbinding NFT is `msg.sender`. + /// @param from The address which owns the asset the NFT is bound to. + /// @param to The address which will own the NFT once unbound. + /// @param tokenId The identifier of the NFT being unbound. + /// @param bindId The identifier of the asset being unbound from. + /// @param data Additional data with no specified format. + /// @return `bytes4(keccak256("onERC721Unbind(address,address,address,uint256,uint256,bytes)"))` + function onERC721Unbind( + address operator, + address from, + address to, + uint256 tokenId, + uint256 bindId, + bytes calldata data + ) external returns (bytes4); + + /// @notice Gets the owner address of the asset represented by id `bindId`. + /// @dev Queries for assets assigned to the zero address MUST throw. + /// @param bindId The identifier of the asset whose owner is being queried. + /// @return The address of the owner of the asset. + function ownerOf(uint256 bindId) external view returns (address); + + /// @notice Checks if an operator can act on behalf of an asset owner. + /// @param owner The address that owns an asset. + /// @param operator The address that acts on behalf of owner `owner`. + /// @return True if `operator` can act on behalf of `owner`, else False. + function isApprovedForAll(address owner, address operator) external view returns (bool); + +} diff --git a/assets/eip-5700/interfaces/IERC721BinderErrors.sol b/assets/eip-5700/interfaces/IERC721BinderErrors.sol new file mode 100644 index 00000000000000..c949e6e57c960c --- /dev/null +++ b/assets/eip-5700/interfaces/IERC721BinderErrors.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-721 Binder Errors Interface +interface IERC721BinderErrors { + + /// @notice Asset binding already exists. + error BindExistent(); + + /// @notice Asset binding is not valid. + error BindInvalid(); + + /// @notice Asset binding does not exist. + error BindNonexistent(); + + /// @notice Originating address does not own the asset. + error OwnerInvalid(); + + /// @notice Receiving address cannot be the zero address. + error ReceiverInvalid(); + + /// @notice Sender is not NFT owner, approved address, or owner operator. + error SenderUnauthorized(); + +} diff --git a/assets/eip-5700/interfaces/IERC721Errors.sol b/assets/eip-5700/interfaces/IERC721Errors.sol new file mode 100644 index 00000000000000..2fc6494c098d5b --- /dev/null +++ b/assets/eip-5700/interfaces/IERC721Errors.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.16; + +/// @title ERC-721 Errors Interface +interface IERC721Errors { + + /// @notice Originating address does not own the NFT. + error OwnerInvalid(); + + /// @notice Receiving address cannot be the zero address. + error ReceiverInvalid(); + + /// @notice Receiving contract does not implement the ERC-721 wallet interface. + error SafeTransferUnsupported(); + + /// @notice Sender is not NFT owner, approved address, or owner operator. + error SenderUnauthorized(); + + /// @notice NFT supply has hit maximum capacity. + error SupplyMaxCapacity(); + + /// @notice Token has already minted. + error TokenAlreadyMinted(); + + /// @notice NFT does not exist. + error TokenNonExistent(); + +} + From 40ef70fd4b94c978de2e2e1a450407a8af20759a Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:30:05 -0500 Subject: [PATCH 179/274] Update EIP-5920: Reduce account creation gas (#6349) --- EIPS/eip-5920.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index 9a899d93eb924d..b2bffafcbded2b 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -26,7 +26,7 @@ Currently, to send ether to an address requires you to call a function of that a | `BASE_GAS_COST` | `8500` | | `WARM_GAS_COST` | `100` | | `COLD_GAS_COST` | `2500` | -| `CREATION_GAS_COST` | `32600` | +| `CREATION_GAS_COST` | `32000` | A new opcode is introduced: `PAY` (`PAY_OPCODE`), which: From e6d47e7600b6d076e09ccdd8c0b9e7dcadc4c159 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 17 Jan 2023 19:59:37 -0500 Subject: [PATCH 180/274] Update EIP-5920: Reference eip-2929 and massively reduce pay opcode price (#6350) * Update EIP-5920: Reference eip-2929 and massively reduce pay opcode price * Add 2929 to requires --- EIPS/eip-5920.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-5920.md b/EIPS/eip-5920.md index b2bffafcbded2b..d442dfa66bf569 100644 --- a/EIPS/eip-5920.md +++ b/EIPS/eip-5920.md @@ -8,6 +8,7 @@ status: Review type: Standards Track category: Core created: 2022-03-14 +requires: 2929 --- ## Abstract @@ -23,17 +24,14 @@ Currently, to send ether to an address requires you to call a function of that a | Parameter | Value | | ------------------- | ------- | | `PAY_OPCODE` | `0xf9` | -| `BASE_GAS_COST` | `8500` | -| `WARM_GAS_COST` | `100` | -| `COLD_GAS_COST` | `2500` | -| `CREATION_GAS_COST` | `32000` | +| `GAS_COST` | `3000` | A new opcode is introduced: `PAY` (`PAY_OPCODE`), which: - Pops two values from the stack: `addr` then `val`. - Transfers `val` wei from the executing address to the address `addr`. If `addr` is the zero address, instead, `val` wei is burned from the executing address. -The cost of this opcode is `BASE_GAS_COST`. If `addr` is not the zero address, this opcode costs an additional `WARM_GAS_COST` if `addr` is a warm account, `COLD_GAS_COST` if `addr` is a cold account, or `CREATION_GAS_COST` if `addr` has not yet been created. +The cost of this opcode is `GAS_COST`. If `addr` is not the zero address, the [EIP-2929](./eip-2929.md) account access costs are also incurred. ## Rationale From 29ed3d5af63010133844b16110e27b9ab784dd97 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Tue, 17 Jan 2023 23:15:58 -0500 Subject: [PATCH 181/274] Update EIP-6190: Remove axic as an author (#6352) --- EIPS/eip-6190.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-6190.md b/EIPS/eip-6190.md index 90fddc4871cfa4..5407aa6991448b 100644 --- a/EIPS/eip-6190.md +++ b/EIPS/eip-6190.md @@ -2,7 +2,7 @@ eip: 6190 title: Verkle-compatible SELFDESTRUCT description: Changes SELFDESTRUCT to only cause a finite number of state changes -author: Pandapip1 (@Pandapip1), Alex Beregszaszi (@axic) +author: Pandapip1 (@Pandapip1) discussions-to: https://ethereum-magicians.org/t/eip-6190-functional-selfdestruct/12232 status: Draft type: Standards Track From b959821529d9b954ae130076ebe39149435cb973 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Tue, 17 Jan 2023 20:27:51 -0800 Subject: [PATCH 182/274] Update EIP-2135: Move to Last Call (#6338) * Update eip-2135.md * Update eip-2135.md --- EIPS/eip-2135.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-2135.md b/EIPS/eip-2135.md index 6c3d32582c5816..811258895652f3 100644 --- a/EIPS/eip-2135.md +++ b/EIPS/eip-2135.md @@ -4,7 +4,8 @@ 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 -status: Review +status: Last Call +last-call-deadline: 2023-02-01 type: Standards Track category: ERC created: 2019-06-23 @@ -163,4 +164,5 @@ Security audits and tests should be used to verify that the access control to th function behaves as expected. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 67558acc2baf5e7a6993bedad0de9b78a9fefde3 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Tue, 17 Jan 2023 20:28:43 -0800 Subject: [PATCH 183/274] Update eip-5982.md (#6340) --- EIPS/eip-5982.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5982.md b/EIPS/eip-5982.md index ec9a80335aff52..1a2be955ca2298 100644 --- a/EIPS/eip-5982.md +++ b/EIPS/eip-5982.md @@ -4,7 +4,7 @@ title: Role-based Access Control description: An interface for role-based access control for smart contracts. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/eip-5982-role-based-access-control/11759 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-11-15 From 39afe1d932ee4c612003a640c1d2a61fbe8c243a Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Tue, 17 Jan 2023 20:30:22 -0800 Subject: [PATCH 184/274] Update eip-5604.md (#6342) --- EIPS/eip-5604.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-5604.md b/EIPS/eip-5604.md index f07521ce8e421c..43b0a60a7a084f 100644 --- a/EIPS/eip-5604.md +++ b/EIPS/eip-5604.md @@ -4,7 +4,7 @@ title: NFT Lien description: Extend EIP-721 to support putting liens on NFT author: Allen Zhou , Alex Qin , Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/creating-a-new-erc-proposal-for-nft-lien/10683 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-09-05 From 6e1cc697fa5fdcf8bd27cdf74e27371e1318eade Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 18 Jan 2023 20:56:28 +0100 Subject: [PATCH 185/274] Update to EIP-5805 (#6356) * change getPastVotes requirements * add CLOCK_MODE * add missing function details * use uint48 timepoints * update from= param for CLOCK_MODE * Update eip-5805.md * Apply suggestions from code review Co-authored-by: Francisco * Add @frangio as a co-author * Update eip-5805.md Co-authored-by: Francisco --- EIPS/eip-5805.md | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-5805.md b/EIPS/eip-5805.md index 657216340d39d7..233fa77c7df22d 100644 --- a/EIPS/eip-5805.md +++ b/EIPS/eip-5805.md @@ -2,7 +2,7 @@ eip: 5805 title: Voting with delegation description: An interface for voting weight tracking, with delegation support -author: Hadrien Croubois (@Amxx) +author: Hadrien Croubois (@Amxx), Francisco Giordano (@frangio) discussions-to: https://ethereum-magicians.org/t/eip-5805-voting-with-delegation/11407 status: Draft type: Standards Track @@ -60,7 +60,7 @@ This function returns the current timepoint. It could be `block.timestamp`, `blo - If operating using **timestamp**, then this function MUST be implemented. - If operating using any other mode, then this function MUST be implemented. -This function is thus optional, and its absence should be considered as a marker of the contract operating using block number. (This makes this EIP compatible with pre-existing voting contracts) +This function is thus optional, and its absence should be considered as a marker of the contract operating using block number. (This makes this EIP compatible with pre-existing voting contracts). ```yaml - name: clock @@ -69,7 +69,33 @@ This function is thus optional, and its absence should be considered as a marker inputs: [] outputs: - name: timepoint - type: uint256 + type: uint48 +``` + +### CLOCK_MODE + +This function returns a string describing the clock the contract is operating on. + +- If operating using **block number**: + - If the block numbers are those of the `NUMBER` opcode (`0x43`), then this function SHOULD be implemented and return `mode=blocknumber&from=default`. + - If it is any other block number, then this function MUST be implemented and return `mode=blocknumber&from=`, where `` is a CAIP-2 Blockchain ID such as `eip155:1`. +- If operating using **timestamp**, then this function MUST be implemented and return `mode=timestamp`. +- If operating using any other mode, then this function MUST be implemented and return a unique identifier for the encoded `mode` field. + +This function is thus optional, and its absence should be considered as a marker of the contract operating using block numbers, which can be clearly idenfitied from the absence of this function. (This makes this EIP compatible with pre-existing voting contracts). + +Note that when operating using **block number**, the `clock()` is expected to returns the value given by the `NUMBER` opcode (`0x43`). In some cases this can be the block number of another chain (in arbitrum, opcode `0x43` returns the block number of the last recorded operation on the parent chain). A contract can use `from=default` to specify that the block number used is the one provided by the `NUMBER` opcode (`0x43`). If a more explicit description is needed, CAIP-2 blockchain id should be used, as shown in the above. + +The return string MUST be formatted like a URL query string (a.k.a. `application/x-www-form-urlencoded`). This allows easy decoding in standard JavaScript with `new URLSearchParams(CLOCK_MODE)`. + +```yaml +- name: CLOCK_MODE + type: function + stateMutability: view + inputs: [] + outputs: + - name: descriptor + type: string ``` #### getVotes @@ -94,7 +120,11 @@ This function MUST be implemented #### getPastVotes -This function returns the historical voting weight of an account. This corresponds to all the voting power delegated to it at a specific timepoint. The timepoint parameter should match the operating mode of the contract. This function SHOULD only serve past checkpoints that are immutable. Calling this function with a timepoint that is greater or equal to `clock()` SHOULD revert. For any timepoint that is strictly smaller than `clock()`, the value returned by `getPastVotes` should be constant. +This function returns the historical voting weight of an account. This corresponds to all the voting power delegated to it at a specific timepoint. The timepoint parameter MUST match the operating mode of the contract. This function SHOULD only serve past checkpoints, which SHOULD be immutable. + +- Calling this function with a timepoint that is greater or equal to `clock()` SHOULD revert. +- Calling this function with a timepoint scrictly smaller than `clock()` SHOULD NOT revert. +- For any integer that is strictly smaller than `clock()`, the value returned by `getPastVotes` SHOULD be constant. This means that for any call to this function that returns a value, re-executing the same call (at any time in the future) SHOULD return the same value. As tokens delegated to `address(0)` should not be counted/snapshoted, `getPastVotes(0,x)` SHOULD always return `0` (for all values of `x`). @@ -375,6 +405,10 @@ Delegation allows token holders to trust a delegate with their vote while keepin The use of checkpoints prevents double voting. Votes, for example in the context of a governance proposal, should rely on a snapshot defined by a timepoint. Only tokens delegated at that timepoint can be used for voting. This means any token transfer performed after the snapshot will not affect the voting power of the sender/receiver's delegate. This also means that in order to vote, someone must acquire tokens and delegate them before the snapshot is taken. Governors can, and do, include a delay between the proposal is submitted and the snapshot is taken so that users can take the necessary actions (change their delegation, buy more tokens, ...). +`clock` returns `uint48` as it is largelly sufficient for storing realistic values. In timestamp mode, `uint48` will be enough until the year 8921556. Even in block number mode, with 10,000 blocks per seconds, it would be enough until the year 2861. Using a type smaller than uint256 allows some storage packing of timepoints with other associated values. Greatly reducing the cost of writting and reading from storage. Depending on the evolution of the blockchain (particularly layer twos), `uint32` might cause issues fairly quickly. On the other hand, anything bigger than `uint48` is overkill. + +While timestamps produced by `clock` are represented as `uint48`, `getPastVotes`'s timepoint argument is `uint256` for backward compatibility. Any timepoint `>=2**48` passed to `getPastVotes` SHOULD cause the function to revert, as it would be a lookup in the future. + `delegateBySig` is necessary to offer a gasless workflow to token holders that do not want to pay gas for voting. The `nonces` mapping is given for replay protection. From dc305bb3078701d894acfd9d901c1da37bfb659c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 20 Jan 2023 11:14:28 +0100 Subject: [PATCH 186/274] update interface (#6361) --- EIPS/eip-5805.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5805.md b/EIPS/eip-5805.md index 233fa77c7df22d..6521b4ff3e3b85 100644 --- a/EIPS/eip-5805.md +++ b/EIPS/eip-5805.md @@ -375,11 +375,13 @@ This MUST be emitted when: ### Solidity interface ```sol -interface IERC_XXXX { +interface IERC5805 { event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); event DelegateVotesChanged(address indexed delegate, uint256 previousBalance, uint256 newBalance); - function clock() external view returns (uint256); + function clock() external view returns (uint48); + function CLOCK_MODE() external view returns (string); + function getVotes(address account) external view returns (uint256); function getPastVotes(address account, uint256 timepoint) external view returns (uint256); function delegates(address account) external view returns (address); From db23d1d125b631c335f68e734da8e107878801a6 Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Fri, 20 Jan 2023 06:43:22 -0800 Subject: [PATCH 187/274] Update EIP-5298: Move to Review (#6343) * Update eip-5298.md * Update eip-5298.md * Update eip-5298.md * Update eip-5298.md * Fix grammar Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-5298.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-5298.md b/EIPS/eip-5298.md index 19a368e1aa05c1..5f885097c80ab4 100644 --- a/EIPS/eip-5298.md +++ b/EIPS/eip-5298.md @@ -4,7 +4,7 @@ title: ENS Trust to hold NFTs under ENS name description: An interface for a smart contract acting as a "trust" that holds tokens by ENS name. author: Zainan Victor Zhou (@xinbenlv) discussions-to: https://ethereum-magicians.org/t/erc-eip-5198-ens-as-token-holder/10374 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-07-12 @@ -30,13 +30,17 @@ interface IERC_ENS_TRUST is ERC721Receiver, ERC1155Receiver { } ``` -3. `claimTo` MUST check if `msg.sender` is the owner of the ENS node (and/or approved by the domain in implementation-specific ways). The compliant contract then MUST make a call to the `safeTransferFrom` function of [EIP-721](./eip-712.md) or [EIP-1155](./eip-1155.md). +3. `claimTo` MUST check if `msg.sender` is the owner of the ENS node identified by `bytes32 ensNode` (and/or approved by the domain in implementation-specific ways). The compliant contract then MUST make a call to the `safeTransferFrom` function of [EIP-721](./eip-712.md) or [EIP-1155](./eip-1155.md). + +4. Any `ensNode` is allowed. ## Rationale 1. ENS was chosen because it is a well-established scoped ownership namespace. This is nonetheless compatible with other scoped ownership namespaces. +2. We didn't expose getters or setters for ensRoot because it is outside of the scope of this EIP. + ## Backwards Compatibility No backward compatibility issues were found. From 74689658b615c51f292dc383194ed7042e9d3f6a Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Fri, 20 Jan 2023 18:33:32 +0100 Subject: [PATCH 188/274] Add EIP-6220: Propose Composable NFTs utilizing Equippable Parts (#6220) * Propose Composable NFTs utilizing Equippable Parts Proposed an interface for Composable non-fungible tokens through fixed and slot parts equipping. * Assign EIP number Assigned EIP number based on proposal PR number. This is following past experience, but can easily be changed if requested by EIP editors. * Add discussion URI * Fix links to test cases * Add a single trailing newline to the proposal * Updated and expanded the proposal * Slightly refined Abstract * Additional refinements * Fix formatting of ordered list in Rationale * Removes configuration files and simplifies package.json. * EIP-6220: Leaves only minimal needed packages to compile and run tests. Co-authored-by: steven2308 --- EIPS/eip-6220.md | 477 ++++ assets/eip-6220/contracts/Catalog.sol | 267 ++ assets/eip-6220/contracts/EquippableToken.sol | 2402 +++++++++++++++++ assets/eip-6220/contracts/ICatalog.sol | 160 ++ assets/eip-6220/contracts/IERC5773.sol | 92 + assets/eip-6220/contracts/IERC6059.sol | 114 + assets/eip-6220/contracts/IEquippable.sol | 194 ++ .../contracts/library/EquippableLib.sol | 29 + .../eip-6220/contracts/mocks/CatalogMock.sol | 41 + .../eip-6220/contracts/mocks/ERC721Mock.sol | 16 + .../contracts/mocks/ERC721ReceiverMock.sol | 16 + .../contracts/mocks/EquippableTokenMock.sol | 100 + .../contracts/mocks/NonReceiverMock.sol | 7 + .../contracts/security/ReentrancyGuard.sol | 77 + .../contracts/utils/EquipRenderUtils.sol | 481 ++++ .../contracts/utils/MultiAssetRenderUtils.sol | 194 ++ assets/eip-6220/hardhat.config.ts | 18 + assets/eip-6220/package.json | 29 + assets/eip-6220/test/catalog.ts | 280 ++ assets/eip-6220/test/equippableFixedParts.ts | 588 ++++ assets/eip-6220/test/equippableSlotParts.ts | 923 +++++++ assets/eip-6220/test/multiasset.ts | 670 +++++ assets/eip-6220/test/nestable.ts | 1180 ++++++++ assets/eip-6220/test/renderUtils.ts | 129 + 24 files changed, 8484 insertions(+) create mode 100644 EIPS/eip-6220.md create mode 100644 assets/eip-6220/contracts/Catalog.sol create mode 100644 assets/eip-6220/contracts/EquippableToken.sol create mode 100644 assets/eip-6220/contracts/ICatalog.sol create mode 100644 assets/eip-6220/contracts/IERC5773.sol create mode 100644 assets/eip-6220/contracts/IERC6059.sol create mode 100644 assets/eip-6220/contracts/IEquippable.sol create mode 100644 assets/eip-6220/contracts/library/EquippableLib.sol create mode 100644 assets/eip-6220/contracts/mocks/CatalogMock.sol create mode 100644 assets/eip-6220/contracts/mocks/ERC721Mock.sol create mode 100644 assets/eip-6220/contracts/mocks/ERC721ReceiverMock.sol create mode 100644 assets/eip-6220/contracts/mocks/EquippableTokenMock.sol create mode 100644 assets/eip-6220/contracts/mocks/NonReceiverMock.sol create mode 100644 assets/eip-6220/contracts/security/ReentrancyGuard.sol create mode 100644 assets/eip-6220/contracts/utils/EquipRenderUtils.sol create mode 100644 assets/eip-6220/contracts/utils/MultiAssetRenderUtils.sol create mode 100644 assets/eip-6220/hardhat.config.ts create mode 100644 assets/eip-6220/package.json create mode 100644 assets/eip-6220/test/catalog.ts create mode 100644 assets/eip-6220/test/equippableFixedParts.ts create mode 100644 assets/eip-6220/test/equippableSlotParts.ts create mode 100644 assets/eip-6220/test/multiasset.ts create mode 100644 assets/eip-6220/test/nestable.ts create mode 100644 assets/eip-6220/test/renderUtils.ts diff --git a/EIPS/eip-6220.md b/EIPS/eip-6220.md new file mode 100644 index 00000000000000..879b32ce9d327c --- /dev/null +++ b/EIPS/eip-6220.md @@ -0,0 +1,477 @@ +--- +eip: 6220 +title: Composable NFTs utilizing Equippable Parts +description: An interface for Composable non-fungible tokens through fixed and slot parts equipping. +author: Bruno Škvorc (@Swader), Cicada (@CicadaNCR), Steven Pineda (@steven2308), Stevan Bogosavljevic (@stevyhacker), Jan Turk (@ThunderDeliverer) +discussions-to: https://ethereum-magicians.org/t/eip-6220-composable-nfts-utilizing-equippable-parts/12289 +status: Draft +type: Standards Track +category: ERC +created: 2022-12-20 +requires: 165, 721, 5773, 6059 +--- + +## Abstract + +The Composable NFTs utilizing equippable parts standard extends [EIP-721](./eip-721.md) by allowing the NFTs to selectively add parts to themselves via equipping. + +Tokens can be composed by cherry picking the list of parts from a Catalog for each NFT instance, and are able to equip other NFTs into slots, which are also defined within the Catalog. Catalogs contain parts from which NFTs can be composed. + +This proposal introduces two types of parts; slot type of parts and fixed type of parts. The slot type of parts allow for other NFT collections to be equipped into them, while fixed parts are full components with their own metadata. + +Equipping a part into an NFT doesn't generate a new token, but rather adds another component to be rendered when retrieving the token. + +## Motivation + +With NFTs being a widespread form of tokens in the Ethereum ecosystem and being used for a variety of use cases, it is time to standardize additional utility for them. Having the ability for tokens to equip other tokens and be composed from a set of available parts allows for greater utility, usability and forward compatibility. + +In the four years since [EIP-721](./eip-721.md) was published, the need for additional functionality has resulted in countless extensions. This EIP improves upon EIP-721 in the following areas: + +- [Composing](#composing) +- [Token progression](#token-progression) +- [Merit tracking](#merit-tracking) +- [Provable Digital Scarcity](#provable-digital-scarcity) + +### Composing + +NFTs can work together to create a greater construct. Prior to this proposal, multiple NFTs could be composed into a single construct either by checking all of the compatible NFTs associated with a given account and used indiscriminately (which could result in unexpected result if there was more than one NFT intended to be used in the same slot), or by keeping a custom ledger of parts to compose together (either in a smart contract or an off-chain database). This proposal establishes a standardized framework for composable NFTs, where a single NFT can select which parts should be a part of the whole, with the information being on chain. Composing NFTs in such a way allows for virtually unbounded customization of the base NFT. An example of this could be a movie NFT. Some parts, like credits, should be fixed. Other parts, like scenes, should be interchangeable, so that various releases (base version, extended cuts, anniversary editions,...) can be replaced. + +### Token progression + +As the token progresses through various stages of its existence, it can attain or be awarded various parts. This can be explained in terms of gaming. A character could be represented by an NFT utilizing this proposal and would be able to equip gear acquired through the gameplay activities and as it progresses further in the game, better items would be available. In stead of having numerous NFTs representing the items collected through its progression, equippable parts can be unlocked and the NFT owner would be able to decide which items to equip and which to keep in the inventory (not equipped) without need of a centralized party. + +### Merit tracking + +An equippable NFT can also be used to track merit. An example of this is academic merit. The equippable NFT in this case would represent a sort of digital portfolio of academic achievements, where the owner would be able to equip their diplomas, published articles and awards for all to see. + +### Provable Digital Scarcity + +The majority of current NFT projects are only mock-scarce. Even with a limited supply of tokens, the utility of these (if any) is uncapped. As an example, you can log into 500 different instances of the same game using the same wallet and the same NFT. You can then equip the same hat onto 500 different in-game avatars at the same time, because its visual representation is just a client-side mechanic. + +This proposal adds the ability to enforce that, if a hat is equipped on one avatar (by being sent into it and then equipped), it cannot be equipped on another. This provides real digital scarcity. + +## 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. + +### Equippable tokens + +The interface of the core smart contract of the equippable tokens. + +```solidity +/// @title EIP-X Composable NFTs utilizing Equippable Parts +/// @dev See https://eips.ethereum.org/EIPS/eip-6220 +/// @dev Note: the ERC-165 identifier for this interface is 0x28bc9ae4. + +pragma solidity ^0.8.16; + +import "./IERC5773.sol"; + +interface IEquippable is IERC5773 { + /** + * @notice Used to store the core structure of the `Equippable` component. + * @return assetId The ID of the asset equipping a child + * @return childAssetId The ID of the asset used as equipment + * @return childId The ID of token that is equipped + * @return childEquippableAddress Address of the collection to which the child asset belongs to + */ + struct Equipment { + uint64 assetId; + uint64 childAssetId; + uint256 childId; + address childEquippableAddress; + } + + /** + * @notice Used to provide a struct for inputing equip data. + * @dev Only used for input and not storage of data. + * @return tokenId ID of the token we are managing + * @return childIndex Index of a child in the list of token's active children + * @return assetId ID of the asset that we are equipping into + * @return slotPartId ID of the slot part that we are using to equip + * @return childAssetId ID of the asset that we are equipping + */ + struct IntakeEquip { + uint256 tokenId; + uint256 childIndex; + uint64 assetId; + uint64 slotPartId; + uint64 childAssetId; + } + + /** + * @notice Used to notify listeners that a child's asset has been equipped into one of its parent assets. + * @param tokenId ID of the token that had an asset equipped + * @param assetId ID of the asset associated with the token we are equipping into + * @param slotPartId ID of the slot we are using to equip + * @param childId ID of the child token we are equipping into the slot + * @param childAddress Address of the child token's collection + * @param childAssetId ID of the asset associated with the token we are equipping + */ + event ChildAssetEquipped( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed slotPartId, + uint256 childId, + address childAddress, + uint64 childAssetId + ); + + /** + * @notice Used to notify listeners that a child's asset has been unequipped from one of its parent assets. + * @param tokenId ID of the token that had an asset unequipped + * @param assetId ID of the asset associated with the token we are unequipping out of + * @param slotPartId ID of the slot we are unequipping from + * @param childId ID of the token being unequipped + * @param childAddress Address of the collection that a token that is being unequipped belongs to + * @param childAssetId ID of the asset associated with the token we are unequipping + */ + event ChildAssetUnequipped( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed slotPartId, + uint256 childId, + address childAddress, + uint64 childAssetId + ); + + /** + * @notice Used to notify listeners that the assets belonging to a `equippableGroupId` have been marked as + * equippable into a given slot and parent + * @param equippableGroupId ID of the equippable group being marked as equippable into the slot associated with + * `slotPartId` of the `parentAddress` collection + * @param slotPartId ID of the slot part of the catalog into which the parts belonging to the equippable group + * associated with `equippableGroupId` can be equipped + * @param parentAddress Address of the collection into which the parts belonging to `equippableGroupId` can be + * equipped + */ + event ValidParentEquippableGroupIdSet( + uint64 indexed equippableGroupId, + uint64 indexed slotPartId, + address parentAddress + ); + + /** + * @notice Used to equip a child into a token. + * @dev The `IntakeEquip` stuct contains the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data An `IntakeEquip` struct specifying the equip data + */ + function equip( + IntakeEquip memory data + ) external; + + /** + * @notice Used to unequip child from parent token. + * @dev This can only be called by the owner of the token or by an account that has been granted permission to + * manage the given token by the current owner. + * @param tokenId ID of the parent from which the child is being unequipped + * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped + * @param slotPartId ID of the `Slot` from which to unequip the child + */ + function unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) external; + + /** + * @notice Used to check whether the token has a given child equipped. + * @dev This is used to prevent from transferring a child that is equipped. + * @param tokenId ID of the parent token for which we are querying for + * @param childAddress Address of the child token's smart contract + * @param childId ID of the child token + * @return bool The boolean value indicating whether the child token is equipped into the given token or not + */ + function isChildEquipped( + uint256 tokenId, + address childAddress, + uint256 childId + ) external view returns (bool); + + /** + * @notice Used to verify whether a token can be equipped into a given parent's slot. + * @param parent Address of the parent token's smart contract + * @param tokenId ID of the token we want to equip + * @param assetId ID of the asset associated with the token we want to equip + * @param slotId ID of the slot that we want to equip the token into + * @return bool The boolean indicating whether the token with the given asset can be equipped into the desired + * slot + */ + function canTokenBeEquippedWithAssetIntoSlot( + address parent, + uint256 tokenId, + uint64 assetId, + uint64 slotId + ) external view returns (bool); + + /** + * @notice Used to get the Equipment object equipped into the specified slot of the desired token. + * @dev The `Equipment` struct consists of the following data: + * [ + * assetId, + * childAssetId, + * childId, + * childEquippableAddress + * ] + * @param tokenId ID of the token for which we are retrieving the equipped object + * @param targetCatalogAddress Address of the `Catalog` associated with the `Slot` part of the token + * @param slotPartId ID of the `Slot` part that we are checking for equipped objects + * @return struct The `Equipment` struct containing data about the equipped object + */ + function getEquipment( + uint256 tokenId, + address targetCatalogAddress, + uint64 slotPartId + ) external view returns (Equipment memory); + + /** + * @notice Used to get the asset and equippable data associated with given `assetId`. + * @param tokenId ID of the token for which to retrieve the asset + * @param assetId ID of the asset of which we are retrieving + * @return metadataURI The metadata URI of the asset + * @return equippableGroupId ID of the equippable group this asset belongs to + * @return catalogAddress The address of the catalog the part belongs to + * @return partIds An array of IDs of parts included in the asset + */ + function getAssetAndEquippableData(uint256 tokenId, uint64 assetId) + external + view + returns ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] calldata partIds + ); +} +``` + +### Catalog + +The interface of the Catalog containing the equippable parts. Catalogs are collections of equippable fixed and slot parts and are not restricted to a single collection, but can support any number of NFT collections. + +```solidity +/** + * @title ICatalog + * @notice An interface Catalog for equippable module. + * @dev Note: the ERC-165 identifier for this interface is 0xd912401f. + */ + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +interface ICatalog is IERC165 { + /** + * @notice Event to announce addition of a new part. + * @dev It is emitted when a new part is added. + * @param partId ID of the part that was added + * @param itemType Enum value specifying whether the part is `None`, `Slot` and `Fixed` + * @param zIndex An uint specifying the z value of the part. It is used to specify the depth which the part should + * be rendered at + * @param equippableAddresses An array of addresses that can equip this part + * @param metadataURI The metadata URI of the part + */ + event AddedPart( + uint64 indexed partId, + ItemType indexed itemType, + uint8 zIndex, + address[] equippableAddresses, + string metadataURI + ); + + /** + * @notice Event to announce new equippables to the part. + * @dev It is emitted when new addresses are marked as equippable for `partId`. + * @param partId ID of the part that had new equippable addresses added + * @param equippableAddresses An array of the new addresses that can equip this part + */ + event AddedEquippables( + uint64 indexed partId, + address[] equippableAddresses + ); + + /** + * @notice Event to announce the overriding of equippable addresses of the part. + * @dev It is emitted when the existing list of addresses marked as equippable for `partId` is overwritten by a new + * one. + * @param partId ID of the part whose list of equippable addresses was overwritten + * @param equippableAddresses The new, full, list of addresses that can equip this part + */ + event SetEquippables(uint64 indexed partId, address[] equippableAddresses); + + /** + * @notice Event to announce that a given part can be equipped by any address. + * @dev It is emitted when a given part is marked as equippable by any. + * @param partId ID of the part marked as equippable by any address + */ + event SetEquippableToAll(uint64 indexed partId); + + /** + * @notice Used to define a type of the item. Possible values are `None`, `Slot` or `Fixed`. + * @dev Used for fixed and slot parts. + */ + enum ItemType { + None, + Slot, + Fixed + } + + /** + * @notice The integral structure of a standard RMRK catalog item defining it. + * @dev Requires a minimum of 3 storage slots per catalog item, equivalent to roughly 60,000 gas as of Berlin hard fork + * (April 14, 2021), though 5-7 storage slots is more realistic, given the standard length of an IPFS URI. This + * will result in between 25,000,000 and 35,000,000 gas per 250 assets--the maximum block size of Ethereum + * mainnet is 30M at peak usage. + * @return itemType The item type of the part + * @return z The z value of the part defining how it should be rendered when presenting the full NFT + * @return equippable The array of addresses allowed to be equipped in this part + * @return metadataURI The metadata URI of the part + */ + struct Part { + ItemType itemType; //1 byte + uint8 z; //1 byte + address[] equippable; //n Collections that can be equipped into this slot + string metadataURI; //n bytes 32+ + } + + /** + * @notice The structure used to add a new `Part`. + * @dev The part is added with specified ID, so you have to make sure that you are using an unused `partId`, + * otherwise the addition of the part vill be reverted. + * @dev The full `IntakeStruct` looks like this: + * [ + * partID, + * [ + * itemType, + * z, + * [ + * permittedCollectionAddress0, + * permittedCollectionAddress1, + * permittedCollectionAddress2 + * ], + * metadataURI + * ] + * ] + * @return partId ID to be assigned to the `Part` + * @return part A `Part` to be added + */ + struct IntakeStruct { + uint64 partId; + Part part; + } + + /** + * @notice Used to return the metadata URI of the associated catalog. + * @return string Base metadata URI + */ + function getMetadataURI() external view returns (string memory); + + /** + * @notice Used to return the `itemType` of the associated catalog + * @return string `itemType` of the associated catalog + */ + function getType() external view returns (string memory); + + /** + * @notice Used to check whether the given address is allowed to equip the desired `Part`. + * @dev Returns true if a collection may equip asset with `partId`. + * @param partId The ID of the part that we are checking + * @param targetAddress The address that we are checking for whether the part can be equipped into it or not + * @return bool The status indicating whether the `targetAddress` can be equipped into `Part` with `partId` or not + */ + function checkIsEquippable(uint64 partId, address targetAddress) + external + view + returns (bool); + + /** + * @notice Used to check if the part is equippable by all addresses. + * @dev Returns true if part is equippable to all. + * @param partId ID of the part that we are checking + * @return bool The status indicating whether the part with `partId` can be equipped by any address or not + */ + function checkIsEquippableToAll(uint64 partId) external view returns (bool); + + /** + * @notice Used to retrieve a `Part` with id `partId` + * @param partId ID of the part that we are retrieving + * @return struct The `Part` struct associated with given `partId` + */ + function getPart(uint64 partId) external view returns (Part memory); + + /** + * @notice Used to retrieve multiple parts at the same time. + * @param partIds An array of part IDs that we want to retrieve + * @return struct An array of `Part` structs associated with given `partIds` + */ + function getParts(uint64[] calldata partIds) + external + view + returns (Part[] memory); +} +``` + +## Rationale + +Designing the proposal, we considered the following questions: + +1. **Why are we using a Catalog in stead of supporting direct NFT equipping?**\ +If NFTs could be directly equipped into other NFTs without any oversight, the resulting composite would be unpredictable. Catalog allows for parts to be pre-verified in order to result in a composite that composes as expected. Another benefit of Catalog is the ability of defining reusable fixed parts. +2. **Why do we propose two types of parts?**\ +Some parts, that are the same for all of the tokens, don't make sense to be represented by individual NFTs, so they can be represented by fixed parts. This reduces the clutter of the owner's wallet as well as introduces an efficient way of disseminating repetitive assets tied to NFTs.\ +The slot parts allow for equipping NFTs into them. This provides the ability to equip unrelated NFT collections into the base NFT after the unrelated collection has been verified to compose properly.\ +Having two parts allows for support of numerous use cases and, since the proposal doesn't enforce the use of both it can be applied in any configuration needed. +3. **Why is a method to get all of the equipped parts not included?**\ +Getting all parts might not be an operation necessary for all implementers. Additionally, it can be added either as an extension, doable with hooks, or can be emulated using an indexer. +4. **Should Catalog be limited to support one NFT collection at a time or be able to support any nunmber of collections?**\ +As the Catalog is designed in a way that is agnostic to the use case using it. It makes sense to support as wide reusability as possible. Having one Catalog supporting multiple collections allows for optimized operation and reduced gas prices when deploying it and setting fixed as well as slot parts. + +### Fixed parts + +Fixed parts are defined and contained in the Catalog. They have their own metadata and are not meant to change through the lifecycle of the NFT. + +A fixed part cannot be replaced. + +The benefit of fixed parts is that they represent equippable parts that can be equipped by any number of tokens in any number of collections and only need to be defined once. + +### Slot parts + +Slot parts are defined and contained in the Catalog. They don't have their own metadata, but rather support equipping of selected NFT collections into them. The tokens equipped into the slots however, contain their own metadata. This allows for an equippable modifialbe content of the base NFT controlled by its owner. As they can be equipped into any number of tokens of any number of collections, they allow for reliable composing of the final tokens by vetting which NFTs can be equipped by a given slot once and then reused any number of times. + +## Backwards Compatibility + +The Equippable token standard has been made compatible with [EIP-721](./eip-721.md) in order to take advantage of the robust tooling available for implementations of EIP-721 and to ensure compatibility with existing EIP-721 infrastructure. + +## Test Cases + +Tests are included in [`equippableFixedParts.ts`](../assets/eip-6220/test/equippableFixedParts.ts) and [`equippableSlotParts.ts`](../assets/eip-6220/test/equippableSlotParts.ts). + +To run them in terminal, you can use the following commands: + +``` +cd ../assets/eip-6220 +npm install +npx hardhat test +``` + +## Reference Implementation + +See [`EquippableToken.sol`](../assets/eip-6220/contracts/EquippableToken.sol). + + +## Security Considerations + +The same security considerations as with [EIP-721](./eip-721.md) apply: hidden logic may be present in any of the functions, including burn, add resource, accept resource, and more. + +Caution is advised when dealing with non-audited contracts. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-6220/contracts/Catalog.sol b/assets/eip-6220/contracts/Catalog.sol new file mode 100644 index 00000000000000..45d522ef8ba4fb --- /dev/null +++ b/assets/eip-6220/contracts/Catalog.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "./ICatalog.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; + +error BadConfig(); +error IdZeroForbidden(); +error PartAlreadyExists(); +error PartDoesNotExist(); +error PartIsNotSlot(); +error ZeroLengthIdsPassed(); + +/** + * @title Catalog + * @author RMRK team + * @notice Catalog contract for RMRK equippable module. + */ +contract Catalog is ICatalog { + using Address for address; + + /** + * @notice Mapping of uint64 `partId` to Catalog `Part` struct + */ + mapping(uint64 => Part) private _parts; + + /** + * @notice Mapping of uint64 `partId` to boolean flag, indicating that a given `Part` can be equippable by any address + */ + mapping(uint64 => bool) private _isEquippableToAll; + + uint64[] private _partIds; + + string private _metadataURI; + string private _type; + + /** + * @notice Used to initialize the catalog. + * @param metadataURI Base metadata URI of the catalog + * @param type_ Type of catalog + */ + constructor(string memory metadataURI, string memory type_) { + _metadataURI = metadataURI; + _type = type_; + } + + /** + * @notice Used to limit execution of functions intended for the `Slot` parts to only execute when used with such + * parts. + * @dev Reverts execution of a function if the part with associated `partId` is uninitailized or is `Fixed`. + * @param partId ID of the part that we want the function to interact with + */ + modifier onlySlot(uint64 partId) { + _onlySlot(partId); + _; + } + + function _onlySlot(uint64 partId) private view { + ItemType itemType = _parts[partId].itemType; + if (itemType == ItemType.None) revert PartDoesNotExist(); + if (itemType == ItemType.Fixed) revert PartIsNotSlot(); + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + returns (bool) + { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == type(ICatalog).interfaceId; + } + + /** + * @inheritdoc ICatalog + */ + function getMetadataURI() external view returns (string memory) { + return _metadataURI; + } + + /** + * @inheritdoc ICatalog + */ + function getType() external view returns (string memory) { + return _type; + } + + /** + * @notice Internal helper function that adds `Part` entries to storage. + * @dev Delegates to { _addPart } below. + * @param partIntake An array of `IntakeStruct` structs, consisting of `partId` and a nested `Part` struct + */ + function _addPartList(IntakeStruct[] calldata partIntake) internal { + uint256 len = partIntake.length; + for (uint256 i; i < len; ) { + _addPart(partIntake[i]); + unchecked { + ++i; + } + } + } + + /** + * @notice Internal function that adds a single `Part` to storage. + * @param partIntake `IntakeStruct` struct consisting of `partId` and a nested `Part` struct + * + */ + function _addPart(IntakeStruct calldata partIntake) internal { + uint64 partId = partIntake.partId; + Part memory part = partIntake.part; + + if (partId == uint64(0)) revert IdZeroForbidden(); + if (_parts[partId].itemType != ItemType.None) + revert PartAlreadyExists(); + if (part.itemType == ItemType.None) revert BadConfig(); + if (part.itemType == ItemType.Fixed && part.equippable.length != 0) + revert BadConfig(); + + _parts[partId] = part; + _partIds.push(partId); + + emit AddedPart( + partId, + part.itemType, + part.z, + part.equippable, + part.metadataURI + ); + } + + /** + * @notice Internal function used to add multiple `equippableAddresses` to a single catalog entry. + * @dev Can only be called on `Part`s of `Slot` type. + * @param partId ID of the `Part` that we are adding the equippable addresses to + * @param equippableAddresses An array of addresses that can be equipped into the `Part` associated with the `partId` + */ + function _addEquippableAddresses( + uint64 partId, + address[] calldata equippableAddresses + ) internal onlySlot(partId) { + if (equippableAddresses.length <= 0) revert ZeroLengthIdsPassed(); + + uint256 len = equippableAddresses.length; + for (uint256 i; i < len; ) { + _parts[partId].equippable.push(equippableAddresses[i]); + unchecked { + ++i; + } + } + delete _isEquippableToAll[partId]; + + emit AddedEquippables(partId, equippableAddresses); + } + + /** + * @notice Internal function used to set the new list of `equippableAddresses`. + * @dev Overwrites existing `equippableAddresses`. + * @dev Can only be called on `Part`s of `Slot` type. + * @param partId ID of the `Part`s that we are overwiting the `equippableAddresses` for + * @param equippableAddresses A full array of addresses that can be equipped into this `Part` + */ + function _setEquippableAddresses( + uint64 partId, + address[] calldata equippableAddresses + ) internal onlySlot(partId) { + if (equippableAddresses.length <= 0) revert ZeroLengthIdsPassed(); + _parts[partId].equippable = equippableAddresses; + delete _isEquippableToAll[partId]; + + emit SetEquippables(partId, equippableAddresses); + } + + /** + * @notice Internal function used to remove all of the `equippableAddresses` for a `Part` associated with the `partId`. + * @dev Can only be called on `Part`s of `Slot` type. + * @param partId ID of the part that we are clearing the `equippableAddresses` from + */ + function _resetEquippableAddresses(uint64 partId) + internal + onlySlot(partId) + { + delete _parts[partId].equippable; + delete _isEquippableToAll[partId]; + + emit SetEquippables(partId, new address[](0)); + } + + /** + * @notice Sets the isEquippableToAll flag to true, meaning that any collection may be equipped into the `Part` with this + * `partId`. + * @dev Can only be called on `Part`s of `Slot` type. + * @param partId ID of the `Part` that we are setting as equippable by any address + */ + function _setEquippableToAll(uint64 partId) internal onlySlot(partId) { + _isEquippableToAll[partId] = true; + emit SetEquippableToAll(partId); + } + + /** + * @inheritdoc ICatalog + */ + function checkIsEquippableToAll(uint64 partId) public view returns (bool) { + return _isEquippableToAll[partId]; + } + + /** + * @inheritdoc ICatalog + */ + function checkIsEquippable(uint64 partId, address targetAddress) + public + view + returns (bool) + { + // If this is equippable to all, we're good + bool isEquippable = _isEquippableToAll[partId]; + + // Otherwise, must check against each of the equippable for the part + if (!isEquippable && _parts[partId].itemType == ItemType.Slot) { + address[] memory equippable = _parts[partId].equippable; + uint256 len = equippable.length; + for (uint256 i; i < len; ) { + if (targetAddress == equippable[i]) { + isEquippable = true; + break; + } + unchecked { + ++i; + } + } + } + return isEquippable; + } + + /** + * @inheritdoc ICatalog + */ + function getPart(uint64 partId) public view returns (Part memory) { + return (_parts[partId]); + } + + /** + * @inheritdoc ICatalog + */ + function getParts(uint64[] calldata partIds) + public + view + returns (Part[] memory) + { + uint256 numParts = partIds.length; + Part[] memory parts = new Part[](numParts); + + for (uint256 i; i < numParts; ) { + uint64 partId = partIds[i]; + parts[i] = _parts[partId]; + unchecked { + ++i; + } + } + + return parts; + } +} diff --git a/assets/eip-6220/contracts/EquippableToken.sol b/assets/eip-6220/contracts/EquippableToken.sol new file mode 100644 index 00000000000000..ea60fbca457d05 --- /dev/null +++ b/assets/eip-6220/contracts/EquippableToken.sol @@ -0,0 +1,2402 @@ +// SPDX-License-Identifier: CC0-1.0 + +//Generally all interactions should propagate downstream + +pragma solidity ^0.8.16; + +import "./ICatalog.sol"; +import "./IEquippable.sol"; +import "./IERC6059.sol"; +import "./library/EquippableLib.sol"; +import "./security/ReentrancyGuard.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/introspection/IERC165.sol"; + +error ApprovalForAssetsToCurrentOwner(); +error ApproveForAssetsCallerIsNotOwnerNorApprovedForAll(); +error AssetAlreadyExists(); +error BadPriorityListLength(); +error CatalogRequiredForParts(); +error ChildAlreadyExists(); +error ChildIndexOutOfRange(); +error EquippableEquipNotAllowedByCatalog(); +error ERC721AddressZeroIsNotaValidOwner(); +error ERC721ApprovalToCurrentOwner(); +error ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); +error ERC721ApproveToCaller(); +error ERC721InvalidTokenId(); +error ERC721MintToTheZeroAddress(); +error ERC721NotApprovedOrOwner(); +error ERC721TokenAlreadyMinted(); +error ERC721TransferFromIncorrectOwner(); +error ERC721TransferToNonReceiverImplementer(); +error ERC721TransferToTheZeroAddress(); +error IdZeroForbidden(); +error IndexOutOfRange(); +error IsNotContract(); +error MaxPendingAssetsReached(); +error MaxPendingChildrenReached(); +error MaxRecursiveBurnsReached(address childContract, uint256 childId); +error MintToNonNestableImplementer(); +error MustUnequipFirst(); +error NestableTooDeep(); +error NestableTransferToDescendant(); +error NestableTransferToNonNestableImplementer(); +error NestableTransferToSelf(); +error NoAssetMatchingId(); +error NotApprovedForAssetsOrOwner(); +error NotApprovedOrDirectOwner(); +error NotEquipped(); +error PendingChildIndexOutOfRange(); +error SlotAlreadyUsed(); +error TargetAssetCannotReceiveSlot(); +error TokenCannotBeEquippedWithAssetIntoSlot(); +error TokenDoesNotHaveAsset(); +error UnexpectedAssetId(); +error UnexpectedChildId(); +error UnexpectedNumberOfAssets(); +error UnexpectedNumberOfChildren(); + +/** + * @title EquippableToken + * @author RMRK team + * @notice Smart contract of the Equippable module. + */ +contract EquippableToken is + Context, + ReentrancyGuard, + IERC165, + IERC721, + IERC6059, + IEquippable +{ + using Address for address; + using EquippableLib for uint64[]; + + // ----------------- ERC721 ------------- + + // Mapping owner address to token count + mapping(address => uint256) private _balances; + + // Mapping from token ID to approver address to approved address + // The approver is necessary so approvals are invalidated for nested children on transfer + // WARNING: If a child NFT returns to a previous root owner, old permissions would be active again + mapping(uint256 => mapping(address => address)) private _tokenApprovals; + + // Mapping from owner to operator approvals + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // ----------------- MULTIASSETS ------------- + + /// Mapping of uint64 Ids to asset metadata + mapping(uint64 => string) private _assets; + + /// Mapping of tokenId to new asset, to asset to be replaced + mapping(uint256 => mapping(uint64 => uint64)) private _assetReplacements; + + /// Mapping of tokenId to an array of active assets + /// @dev Active recurses is unbounded, getting all would reach gas limit at around 30k items + /// so we leave this as internal in case a custom implementation needs to implement pagination + mapping(uint256 => uint64[]) internal _activeAssets; + + /// Mapping of tokenId to an array of pending assets + mapping(uint256 => uint64[]) internal _pendingAssets; + + /// Mapping of tokenId to an array of priorities for active assets + mapping(uint256 => uint16[]) internal _activeAssetPriorities; + + /// Mapping of tokenId to assetId to whether the token has this asset assigned + mapping(uint256 => mapping(uint64 => bool)) private _tokenAssets; + + /// Mapping from owner to operator approvals for assets + mapping(address => mapping(address => bool)) + private _operatorApprovalsForAssets; + + /** + * @notice Mapping from token ID to approver address to approved address for assets. + * @dev The approver is necessary so approvals are invalidated for nested children on transfer. + * @dev WARNING: If a child NFT returns the original root owner, old permissions would be active again. + */ + mapping(uint256 => mapping(address => address)) + private _tokenApprovalsForAssets; + + // ------------------- NESTABLE -------------- + + uint256 private constant _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP = 100; + + // Mapping from token ID to DirectOwner struct + mapping(uint256 => DirectOwner) private _directOwners; + + // Mapping of tokenId to array of active children structs + mapping(uint256 => Child[]) private _activeChildren; + + // Mapping of tokenId to array of pending children structs + mapping(uint256 => Child[]) private _pendingChildren; + + // Mapping of child token address to child token ID to whether they are pending or active on any token + // We might have a first extra mapping from token ID, but since the same child cannot be nested into multiple tokens + // we can strip it for size/gas savings. + mapping(address => mapping(uint256 => uint256)) private _childIsInActive; + + // ------------------- EQUIPPABLE -------------- + + /// Mapping of uint64 asset ID to corresponding catalog address. + mapping(uint64 => address) private _catalogAddresses; + /// Mapping of uint64 ID to asset object. + mapping(uint64 => uint64) private _equippableGroupIds; + /// Mapping of assetId to catalog parts applicable to this asset, both fixed and slot + mapping(uint64 => uint64[]) private _partIds; + + /// Mapping of token ID to catalog address to slot part ID to equipment information. Used to compose an NFT. + mapping(uint256 => mapping(address => mapping(uint64 => Equipment))) + private _equipments; + + /// Mapping of token ID to child (nestable) address to child ID to count of equipped items. Used to check if equipped. + mapping(uint256 => mapping(address => mapping(uint256 => uint8))) + private _equipCountPerChild; + + /// Mapping of `equippableGroupId` to parent contract address and valid `slotId`. + mapping(uint64 => mapping(address => uint64)) private _validParentSlots; + + // -------------------------- MODIFIERS ---------------------------- + + /** + * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. + * @param tokenId ID of the token to check + */ + modifier onlyApprovedOrOwner(uint256 tokenId) { + _onlyApprovedOrOwner(tokenId); + _; + } + + /** + * @notice Used to verify that the caller is approved to manage the given token or is its direct owner. + * @param tokenId ID of the token to check + */ + modifier onlyApprovedOrDirectOwner(uint256 tokenId) { + _onlyApprovedOrDirectOwner(tokenId); + _; + } + + /** + * @notice Used to ensure that the caller is either the owner of the given token or approved to manage the token's assets + * of the owner. + * @dev If that is not the case, the execution of the function will be reverted. + * @param tokenId ID of the token that we are checking + */ + modifier onlyApprovedForAssetsOrOwner(uint256 tokenId) { + _onlyApprovedForAssetsOrOwner(tokenId); + _; + } + + // --------------------- ERC721 GETTERS --------------------- + + /** + * @notice Used to retrieve the root owner of the given token. + * @dev Root owner is always the externally owned account. + * @dev If the given token is owned by another token, it will recursively query the parent tokens until reaching the + * root owner. + * @param tokenId ID of the token for which the root owner is being retrieved + * @return address Address of the root owner of the given token + */ + function ownerOf( + uint256 tokenId + ) public view virtual override(IERC6059, IERC721) returns (address) { + (address owner, uint256 ownerTokenId, bool isNft) = directOwnerOf( + tokenId + ); + if (isNft) { + owner = IERC6059(owner).ownerOf(ownerTokenId); + } + return owner; + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual returns (bool) { + return + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC5773).interfaceId || + interfaceId == type(IERC6059).interfaceId || + interfaceId == type(IEquippable).interfaceId; + } + + /** + * @inheritdoc IERC721 + */ + function balanceOf(address owner) public view virtual returns (uint256) { + if (owner == address(0)) revert ERC721AddressZeroIsNotaValidOwner(); + return _balances[owner]; + } + + /** + * @inheritdoc IERC721 + */ + function getApproved( + uint256 tokenId + ) public view virtual returns (address) { + _requireMinted(tokenId); + + return _tokenApprovals[tokenId][ownerOf(tokenId)]; + } + + /** + * @inheritdoc IERC721 + */ + function isApprovedForAll( + address owner, + address operator + ) public view virtual returns (bool) { + return _operatorApprovals[owner][operator]; + } + + // --------------------- ERC721 SETTERS --------------------- + + /** + * @inheritdoc IERC721 + */ + function approve(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + if (to == owner) revert ERC721ApprovalToCurrentOwner(); + + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) + revert ERC721ApproveCallerIsNotOwnerNorApprovedForAll(); + + _approve(to, tokenId); + } + + /** + * @inheritdoc IERC721 + */ + function setApprovalForAll(address operator, bool approved) public virtual { + if (_msgSender() == operator) revert ERC721ApproveToCaller(); + _operatorApprovals[_msgSender()][operator] = approved; + emit ApprovalForAll(_msgSender(), operator, approved); + } + + /** + * @notice Used to burn a given token. + * @param tokenId ID of the token to burn + */ + function burn(uint256 tokenId) public virtual { + burn(tokenId, 0); + } + + /** + * @notice Used to burn a token. + * @dev When a token is burned, its children are recursively burned as well. + * @dev The approvals are cleared when the token is burned. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits a {Transfer} event. + * @param tokenId ID of the token to burn + * @param maxChildrenBurns Maximum children to recursively burn + * @return uint256 The number of recursive burns it took to burn all of the children + */ + function burn( + uint256 tokenId, + uint256 maxChildrenBurns + ) public virtual onlyApprovedOrDirectOwner(tokenId) returns (uint256) { + return _burn(tokenId, maxChildrenBurns); + } + + /** + * @inheritdoc IERC721 + */ + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _transfer(from, to, tokenId); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public virtual { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _safeTransfer(from, to, tokenId, data); + } + + // --------------------- ERC721 INTERNAL --------------------- + + /** + * @notice Used to grant an approval to manage a given token. + * @dev Emits an {Approval} event. + * @param to Address to which the approval is being granted + * @param tokenId ID of the token for which the approval is being granted + */ + function _approve(address to, uint256 tokenId) internal virtual { + address owner = ownerOf(tokenId); + _tokenApprovals[tokenId][owner] = to; + emit Approval(owner, to, tokenId); + } + + /** + * @notice Used to update the owner of the token and clear the approvals associated with the previous owner. + * @dev The `destinationId` should equal `0` if the new owner is an externally owned account. + * @param tokenId ID of the token being updated + * @param destinationId ID of the token to receive the given token + * @param to Address of account to receive the token + * @param isNft A boolean value signifying whether the new owner is a token (`true`) or externally owned account + * (`false`) + */ + function _updateOwnerAndClearApprovals( + uint256 tokenId, + uint256 destinationId, + address to, + bool isNft + ) internal { + _directOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId, + isNft: isNft + }); + + // Clear approvals from the previous owner + _approve(address(0), tokenId); + _approveForAssets(address(0), tokenId); + } + + /** + * @notice Used to enforce that the given token has been minted. + * @dev Reverts if the `tokenId` has not been minted yet. + * @dev The validation checks whether the owner of a given token is a `0x0` address and considers it not minted if + * it is. This means that both tokens that haven't been minted yet as well as the ones that have already been + * burned will cause the transaction to be reverted. + * @param tokenId ID of the token to check + */ + function _requireMinted(uint256 tokenId) internal view virtual { + if (!_exists(tokenId)) revert ERC721InvalidTokenId(); + } + + /** + * @notice Used to check whether the given token exists. + * @dev Tokens start existing when they are minted (`_mint`) and stop existing when they are burned (`_burn`). + * @param tokenId ID of the token being checked + * @return bool The boolean value signifying whether the token exists + */ + function _exists(uint256 tokenId) internal view virtual returns (bool) { + return _directOwners[tokenId].ownerAddress != address(0); + } + + /** + * @notice Used to invoke {IERC721Receiver-onERC721Received} on a target address. + * @dev The call is not executed if the target address is not a contract. + * @param from Address representing the previous owner of the given token + * @param to Yarget address that will receive the tokens + * @param tokenId ID of the token to be transferred + * @param data Optional data to send along with the call + * @return bool Boolean value signifying whether the call correctly returned the expected magic value + */ + function _checkOnERC721Received( + address from, + address to, + uint256 tokenId, + bytes memory data + ) private returns (bool) { + if (to.isContract()) { + try + IERC721Receiver(to).onERC721Received( + _msgSender(), + from, + tokenId, + data + ) + returns (bytes4 retval) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721TransferToNonReceiverImplementer(); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @notice Used to safely mint a token to a specified address. + * @dev Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * @dev Emits a {Transfer} event. + * @param to Address to which to safely mint the gven token + * @param tokenId ID of the token to mint to the specified address + */ + function _safeMint(address to, uint256 tokenId) internal virtual { + _safeMint(to, tokenId, ""); + } + + /** + * @notice Used to safely mint the token to the specified address while passing the additional data to contract + * recipients. + * @param to Address to which to mint the token + * @param tokenId ID of the token to mint + * @param data Additional data to send with the tokens + */ + function _safeMint( + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _mint(to, tokenId); + if (!_checkOnERC721Received(address(0), to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @notice Used to mint a specified token to a given address. + * @dev WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible. + * @dev Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * @dev Emits a {Transfer} event. + * @param to Address to mint the token to + * @param tokenId ID of the token to mint + */ + function _mint(address to, uint256 tokenId) internal virtual { + _innerMint(to, tokenId, 0); + + emit Transfer(address(0), to, tokenId); + emit NestTransfer(address(0), to, 0, 0, tokenId); + + _afterTokenTransfer(address(0), to, tokenId); + _afterNestedTokenTransfer(address(0), to, 0, 0, tokenId); + } + + /** + * @notice Used to mint a child token to a given parent token. + * @param to Address of the collection smart contract of the token into which to mint the child token + * @param tokenId ID of the token to mint + * @param destinationId ID of the token into which to mint the new child token + * @param data Additional data with no specified format, sent in the addChild call + */ + function _nestMint( + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) internal virtual { + // It seems redundant, but otherwise it would revert with no error + if (!to.isContract()) revert IsNotContract(); + if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId)) + revert MintToNonNestableImplementer(); + + _innerMint(to, tokenId, destinationId); + _sendToNFT(address(0), to, 0, destinationId, tokenId, data); + } + + /** + * @notice Used to mint a child token into a given parent token. + * @dev Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` must not exist. + * - `tokenId` must not be `0`. + * @param to Address of the collection smart contract of the token into which to mint the child token + * @param tokenId ID of the token to mint + * @param destinationId ID of the token into which to mint the new token + */ + function _innerMint( + address to, + uint256 tokenId, + uint256 destinationId + ) private { + if (to == address(0)) revert ERC721MintToTheZeroAddress(); + if (_exists(tokenId)) revert ERC721TokenAlreadyMinted(); + if (tokenId == 0) revert IdZeroForbidden(); + + _beforeTokenTransfer(address(0), to, tokenId); + _beforeNestedTokenTransfer(address(0), to, 0, destinationId, tokenId); + + _balances[to] += 1; + _directOwners[tokenId] = DirectOwner({ + ownerAddress: to, + tokenId: destinationId, + isNft: destinationId != 0 + }); + } + + /** + * @notice Used to burn a token. + * @dev When a token is burned, its children are recursively burned as well. + * @dev The approvals are cleared when the token is burned. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits a {Transfer} event. + * @dev Emits a {NestTransfer} event. + * @param tokenId ID of the token to burn + * @param maxChildrenBurns Maximum children to recursively burn + * @return uint256 The number of recursive burns it took to burn all of the children + */ + function _burn( + uint256 tokenId, + uint256 maxChildrenBurns + ) internal virtual returns (uint256) { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + address owner = ownerOf(tokenId); + _balances[immediateOwner] -= 1; + + _beforeTokenTransfer(owner, address(0), tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId + ); + + _approve(address(0), tokenId); + _approveForAssets(address(0), tokenId); + + Child[] memory children = childrenOf(tokenId); + + delete _activeChildren[tokenId]; + delete _pendingChildren[tokenId]; + delete _tokenApprovals[tokenId][owner]; + + uint256 pendingRecursiveBurns; + uint256 totalChildBurns; + + uint256 length = children.length; //gas savings + for (uint256 i; i < length; ) { + if (totalChildBurns >= maxChildrenBurns) + revert MaxRecursiveBurnsReached( + children[i].contractAddress, + children[i].tokenId + ); + delete _childIsInActive[children[i].contractAddress][ + children[i].tokenId + ]; + unchecked { + // At this point we know pendingRecursiveBurns must be at least 1 + pendingRecursiveBurns = maxChildrenBurns - totalChildBurns; + } + // We substract one to the next level to count for the token being burned, then add it again on returns + // This is to allow the behavior of 0 recursive burns meaning only the current token is deleted. + totalChildBurns += + IERC6059(children[i].contractAddress).burn( + children[i].tokenId, + pendingRecursiveBurns - 1 + ) + + 1; + unchecked { + ++i; + } + } + // Can't remove before burning child since child will call back to get root owner + delete _directOwners[tokenId]; + + _afterTokenTransfer(owner, address(0), tokenId); + _afterNestedTokenTransfer( + immediateOwner, + address(0), + parentId, + 0, + tokenId + ); + emit Transfer(owner, address(0), tokenId); + emit NestTransfer(immediateOwner, address(0), parentId, 0, tokenId); + + return totalChildBurns; + } + + /** + * @notice Used to safely transfer the token form `from` to `to`. + * @dev The function checks that contract recipients are aware of the ERC721 protocol to prevent tokens from being + * forever locked. + * @dev This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. implement alternative + * mechanisms to perform token transfer, such as signature-based. + * @dev Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer. + * @dev Emits a {Transfer} event. + * @param from Address of the account currently owning the given token + * @param to Address to transfer the token to + * @param tokenId ID of the token to transfer + * @param data Additional data with no specified format, sent in call to `to` + */ + function _safeTransfer( + address from, + address to, + uint256 tokenId, + bytes memory data + ) internal virtual { + _transfer(from, to, tokenId); + if (!_checkOnERC721Received(from, to, tokenId, data)) + revert ERC721TransferToNonReceiverImplementer(); + } + + /** + * @notice Used to transfer the token from `from` to `to`. + * @dev As opposed to {transferFrom}, this imposes no restrictions on msg.sender. + * @dev Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * @dev Emits a {Transfer} event. + * @param from Address of the account currently owning the given token + * @param to Address to transfer the token to + * @param tokenId ID of the token to transfer + */ + function _transfer( + address from, + address to, + uint256 tokenId + ) internal virtual { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(0)) revert ERC721TransferToTheZeroAddress(); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); + + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, 0, to, false); + _balances[to] += 1; + + emit Transfer(from, to, tokenId); + emit NestTransfer(immediateOwner, to, parentId, 0, tokenId); + + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer(immediateOwner, to, parentId, 0, tokenId); + } + + // --------------------- NESTABLE GETTERS --------------------- + + /** + * @notice Used to retrieve the immediate owner of the given token. + * @dev In the event the NFT is owned by an externally owned account, `tokenId` will be `0` and `isNft` will be + * `false`. + * @param tokenId ID of the token for which the immediate owner is being retrieved + * @return address Address of the immediate owner. If the token is owned by an externally owned account, its address + * will be returned. If the token is owned by another token, the parent token's collection smart contract address + * is returned + * @return uint256 Token ID of the immediate owner. If the immediate owner is an externally owned account, the value + * should be `0` + * @return bool A boolean value signifying whether the immediate owner is a token (`true`) or not (`false`) + */ + function directOwnerOf( + uint256 tokenId + ) public view virtual returns (address, uint256, bool) { + DirectOwner memory owner = _directOwners[tokenId]; + if (owner.ownerAddress == address(0)) revert ERC721InvalidTokenId(); + + return (owner.ownerAddress, owner.tokenId, owner.isNft); + } + + /** + * @notice Used to retrieve the active child tokens of a given parent token. + * @dev Returns array of Child structs existing for parent token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the active child tokens + * @return struct[] An array of Child structs containing the parent token's active child tokens + */ + + function childrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory) { + Child[] memory children = _activeChildren[parentId]; + return children; + } + + /** + * @notice Used to retrieve the pending child tokens of a given parent token. + * @dev Returns array of pending Child structs existing for given parent. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which to retrieve the pending child tokens + * @return struct[] An array of Child structs containing the parent token's pending child tokens + */ + + function pendingChildrenOf( + uint256 parentId + ) public view virtual returns (Child[] memory) { + Child[] memory pendingChildren = _pendingChildren[parentId]; + return pendingChildren; + } + + /** + * @notice Used to retrieve a specific active child token for a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the child is being retrieved + * @param index Index of the child token in the parent token's active child tokens array + * @return struct A Child struct containing data about the specified child + */ + function childOf( + uint256 parentId, + uint256 index + ) public view virtual returns (Child memory) { + if (childrenOf(parentId).length <= index) revert ChildIndexOutOfRange(); + Child memory child = _activeChildren[parentId][index]; + return child; + } + + /** + * @notice Used to retrieve a specific pending child token from a given parent token. + * @dev Returns a single Child struct locating at `index` of parent token's active child tokens array. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param parentId ID of the parent token for which the pending child token is being retrieved + * @param index Index of the child token in the parent token's pending child tokens array + * @return struct A Child struct containting data about the specified child + */ + function pendingChildOf( + uint256 parentId, + uint256 index + ) public view virtual returns (Child memory) { + if (pendingChildrenOf(parentId).length <= index) + revert PendingChildIndexOutOfRange(); + Child memory child = _pendingChildren[parentId][index]; + return child; + } + + /** + * @notice Used to verify that the given child tokwn is included in an active array of a token. + * @param childAddress Address of the given token's collection smart contract + * @param childId ID of the child token being checked + * @return bool A boolean value signifying whether the given child token is included in an active child tokens array + * of a token (`true`) or not (`false`) + */ + function childIsInActive( + address childAddress, + uint256 childId + ) public view virtual returns (bool) { + return _childIsInActive[childAddress][childId] != 0; + } + + // --------------------- NESTABLE SETTERS --------------------- + /** + * @notice Used to add a child token to a given parent token. + * @dev This adds the iichild token into the given parent token's pending child tokens array. + * @dev You MUST NOT call this method directly. To add a a child to an NFT you must use either + * `nestTransfer`, `nestMint` or `transferChild` to the NFT. + * @dev Requirements: + * + * - `ownerOf` on the child contract must resolve to the called contract. + * - The pending array of the parent contract must not be full. + * @param parentId ID of the parent token to receive the new child token + * @param childId ID of the new proposed child token + * @param data Additional data with no specified format + */ + function addChild( + uint256 parentId, + uint256 childId, + bytes memory data + ) public virtual { + _requireMinted(parentId); + + address childAddress = _msgSender(); + if (!childAddress.isContract()) revert IsNotContract(); + + Child memory child = Child({ + contractAddress: childAddress, + tokenId: childId + }); + + _beforeAddChild(parentId, childAddress, childId); + + uint256 length = pendingChildrenOf(parentId).length; + + if (length < 128) { + _pendingChildren[parentId].push(child); + } else { + revert MaxPendingChildrenReached(); + } + + // Previous length matches the index for the new child + emit ChildProposed(parentId, length, childAddress, childId); + + _afterAddChild(parentId, childAddress, childId); + } + + /** + * @notice @notice Used to accept a pending child token for a given parent token. + * @dev This moves the child token from parent token's pending child tokens array into the active child tokens + * array. + * @param parentId ID of the parent token for which the child token is being accepted + * @param childIndex Index of a child tokem in the given parent's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) public virtual onlyApprovedOrOwner(parentId) { + _acceptChild(parentId, childIndex, childAddress, childId); + } + + /** + * @notice Used to reject all pending children of a given parent token. + * @dev Removes the children from the pending array mapping. + * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the + * rootOwner of the previous parent. + * @param tokenId ID of the parent token for which to reject all of the pending tokens + */ + function rejectAllChildren( + uint256 tokenId, + uint256 maxRejections + ) public virtual onlyApprovedOrOwner(tokenId) { + _rejectAllChildren(tokenId, maxRejections); + } + + /** + * @notice Used to transfer a child token from a given parent token. + * @param tokenId ID of the parent token from which the child token is being transferred + * @param to Address to which to transfer the token to + * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) + * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or + * pending array) + * @param childAddress Address of the child token's collection smart contract. + * @param childId ID of the child token in its own collection smart contract. + * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the + * parent token (`true`) or in the active array (`false`) + * @param data Additional data with no specified format, sent in call to `_to` + */ + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) public virtual onlyApprovedOrOwner(tokenId) { + _transferChild( + tokenId, + to, + destinationId, + childIndex, + childAddress, + childId, + isPending, + data + ); + } + + /** + * @notice Used to transfer the token into another token. + * @dev The destination token MUST NOT be a child token of the token being transferred or one of its downstream + * child tokens. + * @param from Address of the direct owner of the token to be transferred + * @param to Address of the receiving token's collection smart contract + * @param tokenId ID of the token being transferred + * @param destinationId ID of the token to receive the token being transferred + */ + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) public virtual onlyApprovedOrDirectOwner(tokenId) { + _nestTransfer(from, to, tokenId, destinationId, data); + } + + // --------------------- NESTABLE INTERNAL --------------------- + + /** + * @notice Used to transfer a child token from a given parent token. + * @dev When transferring a child token, the owner of the token is set to `to`, or is not updated in the event of `to` + * being the `0x0` address. + * @dev Requirements: + * + * - `tokenId` must exist. + * @dev Emits {ChildTransferred} event. + * @param tokenId ID of the parent token from which the child token is being transferred + * @param to Address to which to transfer the token to + * @param destinationId ID of the token to receive this child token (MUST be 0 if the destination is not a token) + * @param childIndex Index of a token we are transferring, in the array it belongs to (can be either active array or + * pending array) + * @param childAddress Address of the child token's collection smart contract. + * @param childId ID of the child token in its own collection smart contract. + * @param isPending A boolean value indicating whether the child token being transferred is in the pending array of the + * parent token (`true`) or in the active array (`false`) + * @param data Additional data with no specified format, sent in call to `_to` + */ + function _transferChild( + uint256 tokenId, + address to, + uint256 destinationId, // newParentId + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) internal virtual { + Child memory child; + if (!isPending) { + if (isChildEquipped(tokenId, childAddress, childId)) + revert MustUnequipFirst(); + } + if (isPending) { + child = pendingChildOf(tokenId, childIndex); + } else { + child = childOf(tokenId, childIndex); + } + _checkExpectedChild(child, childAddress, childId); + + _beforeTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + + if (isPending) { + _removeChildByIndex(_pendingChildren[tokenId], childIndex); + } else { + delete _childIsInActive[childAddress][childId]; + _removeChildByIndex(_activeChildren[tokenId], childIndex); + } + + if (to != address(0)) { + if (destinationId == 0) { + IERC721(childAddress).safeTransferFrom( + address(this), + to, + childId, + data + ); + } else { + // Destination is an NFT + IERC6059(child.contractAddress).nestTransferFrom( + address(this), + to, + child.tokenId, + destinationId, + data + ); + } + } + + emit ChildTransferred( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + _afterTransferChild( + tokenId, + childIndex, + childAddress, + childId, + isPending + ); + } + + /** + * @notice Used to accept a pending child token for a given parent token. + * @dev This moves the child token from parent token's pending child tokens array into the active child tokens + * array. + * @dev Requirements: + * + * - `tokenId` must exist + * - `index` must be in range of the pending children array + * @param parentId ID of the parent token for which the child token is being accepted + * @param childIndex Index of a child tokem in the given parent's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual { + if (pendingChildrenOf(parentId).length <= childIndex) + revert PendingChildIndexOutOfRange(); + + Child memory child = pendingChildOf(parentId, childIndex); + _checkExpectedChild(child, childAddress, childId); + if (_childIsInActive[childAddress][childId] != 0) + revert ChildAlreadyExists(); + + _beforeAcceptChild(parentId, childIndex, childAddress, childId); + + // Remove from pending: + _removeChildByIndex(_pendingChildren[parentId], childIndex); + + // Add to active: + _activeChildren[parentId].push(child); + _childIsInActive[childAddress][childId] = 1; // We use 1 as true + + emit ChildAccepted(parentId, childIndex, childAddress, childId); + + _afterAcceptChild(parentId, childIndex, childAddress, childId); + } + + /** + * @notice Used to reject all pending children of a given parent token. + * @dev Removes the children from the pending array mapping. + * @dev This does not update the ownership storage data on children. If necessary, ownership can be reclaimed by the + * rootOwner of the previous parent. + * @dev Requirements: + * + * - `tokenId` must exist + * @param tokenId ID of the parent token for which to reject all of the pending tokens. + * @param maxRejections Maximum number of expected children to reject, used to prevent from + * rejecting children which arrive just before this operation. + */ + function _rejectAllChildren( + uint256 tokenId, + uint256 maxRejections + ) internal virtual { + if (_pendingChildren[tokenId].length > maxRejections) + revert UnexpectedNumberOfChildren(); + + _beforeRejectAllChildren(tokenId); + delete _pendingChildren[tokenId]; + emit AllChildrenRejected(tokenId); + _afterRejectAllChildren(tokenId); + } + + function _checkExpectedChild( + Child memory child, + address expectedAddress, + uint256 expectedId + ) private pure { + if ( + expectedAddress != child.contractAddress || + expectedId != child.tokenId + ) revert UnexpectedChildId(); + } + + /** + * @notice Used to remove a specified child token form an array using its index within said array. + * @dev The caller must ensure that the length of the array is valid compared to the index passed. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @param array An array od Child struct containing info about the child tokens in a given child tokens array + * @param index An index of the child token to remove in the accompanying array + */ + function _removeChildByIndex(Child[] storage array, uint256 index) private { + array[index] = array[array.length - 1]; + array.pop(); + } + + /** + * @notice Used to transfer a token into another token. + * @dev Attempting to nest a token into `0x0` address will result in reverted transaction. + * @dev Attempting to nest a token into itself will result in reverted transaction. + * @param from Address of the account currently owning the given token + * @param to Address of the receiving token's collection smart contract + * @param tokenId ID of the token to transfer + * @param destinationId ID of the token receiving the given token + * @param data Additional data with no specified format, sent in the addChild call + */ + function _nestTransfer( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) internal virtual { + (address immediateOwner, uint256 parentId, ) = directOwnerOf(tokenId); + if (immediateOwner != from) revert ERC721TransferFromIncorrectOwner(); + if (to == address(0)) revert ERC721TransferToTheZeroAddress(); + if (to == address(this) && tokenId == destinationId) + revert NestableTransferToSelf(); + + // Destination contract checks: + // It seems redundant, but otherwise it would revert with no error + if (!to.isContract()) revert IsNotContract(); + if (!IERC165(to).supportsInterface(type(IERC6059).interfaceId)) + revert NestableTransferToNonNestableImplementer(); + _checkForInheritanceLoop(tokenId, to, destinationId); + + _beforeTokenTransfer(from, to, tokenId); + _beforeNestedTokenTransfer( + immediateOwner, + to, + parentId, + destinationId, + tokenId + ); + _balances[from] -= 1; + _updateOwnerAndClearApprovals(tokenId, destinationId, to, true); + _balances[to] += 1; + + // Sending to NFT: + _sendToNFT(immediateOwner, to, parentId, destinationId, tokenId, data); + } + + /** + * @notice Used to send a token to another token. + * @dev If the token being sent is currently owned by an externally owned account, the `parentId` should equal `0`. + * @dev Emits {Transfer} event. + * @dev Emits {NestTransfer} event. + * @param from Address from which the token is being sent + * @param to Address of the collection smart contract of the token to receive the given token + * @param parentId ID of the current parent token of the token being sent + * @param destinationId ID of the tokento receive the token being sent + * @param tokenId ID of the token being sent + * @param data Additional data with no specified format, sent in the addChild call + */ + function _sendToNFT( + address from, + address to, + uint256 parentId, + uint256 destinationId, + uint256 tokenId, + bytes memory data + ) private { + IERC6059 destContract = IERC6059(to); + destContract.addChild(destinationId, tokenId, data); + _afterTokenTransfer(from, to, tokenId); + _afterNestedTokenTransfer(from, to, parentId, destinationId, tokenId); + + emit Transfer(from, to, tokenId); + emit NestTransfer(from, to, parentId, destinationId, tokenId); + } + + /** + * @notice Used to check if nesting a given token into a specified token would create an inheritance loop. + * @dev If a loop would occur, the tokens would be unmanageable, so the execution is reverted if one is detected. + * @dev The check for inheritance loop is bounded to guard against too much gas being consumed. + * @param currentId ID of the token that would be nested + * @param targetContract Address of the collection smart contract of the token into which the given token would be + * nested + * @param targetId ID of the token into which the given token would be nested + */ + function _checkForInheritanceLoop( + uint256 currentId, + address targetContract, + uint256 targetId + ) private view { + for (uint256 i; i < _MAX_LEVELS_TO_CHECK_FOR_INHERITANCE_LOOP; ) { + ( + address nextOwner, + uint256 nextOwnerTokenId, + bool isNft + ) = IERC6059(targetContract).directOwnerOf(targetId); + // If there's a final address, we're good. There's no loop. + if (!isNft) { + return; + } + // Ff the current nft is an ancestor at some point, there is an inheritance loop + if (nextOwner == address(this) && nextOwnerTokenId == currentId) { + revert NestableTransferToDescendant(); + } + // We reuse the parameters to save some contract size + targetContract = nextOwner; + targetId = nextOwnerTokenId; + unchecked { + ++i; + } + } + revert NestableTooDeep(); + } + + // --------------------- MULTIASSET GETTERS --------------------- + + /** + * @notice Used to get the address of the user that is approved to manage the specified token from the current + * owner. + * @param tokenId ID of the token we are checking + * @return address Address of the account that is approved to manage the token + */ + function getApprovedForAssets( + uint256 tokenId + ) public view virtual returns (address) { + _requireMinted(tokenId); + return _tokenApprovalsForAssets[tokenId][ownerOf(tokenId)]; + } + + /** + * @inheritdoc IERC5773 + */ + function getAssetMetadata( + uint256 tokenId, + uint64 assetId + ) public view virtual returns (string memory) { + if (!_tokenAssets[tokenId][assetId]) revert TokenDoesNotHaveAsset(); + return _assets[assetId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getActiveAssets( + uint256 tokenId + ) public view virtual returns (uint64[] memory) { + return _activeAssets[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getPendingAssets( + uint256 tokenId + ) public view virtual returns (uint64[] memory) { + return _pendingAssets[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getActiveAssetPriorities( + uint256 tokenId + ) public view virtual returns (uint16[] memory) { + return _activeAssetPriorities[tokenId]; + } + + /** + * @inheritdoc IERC5773 + */ + function getAssetReplacements( + uint256 tokenId, + uint64 newAssetId + ) public view virtual returns (uint64) { + return _assetReplacements[tokenId][newAssetId]; + } + + /** + * @inheritdoc IERC5773 + */ + function isApprovedForAllForAssets( + address owner, + address operator + ) public view virtual returns (bool) { + return _operatorApprovalsForAssets[owner][operator]; + } + + // --------------------- MULTIASSET SETTERS --------------------- + /** + * @notice Used to grant approvals for specific tokens to a specified address. + * @dev This can only be called by the owner of the token or by an account that has been granted permission to + * manage all of the owner's assets. + * @param to Address of the account to receive the approval to the specified token + * @param tokenId ID of the token for which we are granting the permission + */ + function approveForAssets(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + if (to == owner) revert ApprovalForAssetsToCurrentOwner(); + + if ( + _msgSender() != owner && + !isApprovedForAllForAssets(owner, _msgSender()) + ) revert ApproveForAssetsCallerIsNotOwnerNorApprovedForAll(); + _approveForAssets(to, tokenId); + } + + /** + * @inheritdoc IERC5773 + */ + function setApprovalForAllForAssets( + address operator, + bool approved + ) public virtual { + address owner = _msgSender(); + if (owner == operator) revert ApprovalForAssetsToCurrentOwner(); + + _operatorApprovalsForAssets[owner][operator] = approved; + emit ApprovalForAllForAssets(owner, operator, approved); + } + + /** + * @notice Accepts a asset at from the pending array of given token. + * @dev Migrates the asset from the token's pending asset array to the token's active asset array. + * @dev Active assets cannot be removed by anyone, but can be replaced by a new asset. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending asset array. + * @dev Emits an {AssetAccepted} event. + * @param tokenId ID of the token for which to accept the pending asset + * @param index Index of the asset in the pending array to accept + */ + function acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _acceptAsset(tokenId, index, assetId); + } + + /** + * @notice Rejects a asset from the pending array of given token. + * @dev Removes the asset from the token's pending asset array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - `index` must be in range of the length of the pending asset array. + * @dev Emits a {AssetRejected} event. + * @param tokenId ID of the token that the asset is being rejected from + * @param index Index of the asset in the pending array to be rejected + */ + function rejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _rejectAsset(tokenId, index, assetId); + } + + /** + * @notice Rejects all assets from the pending array of a given token. + * @dev Effecitvely deletes the pending array. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * @dev Emits a {AssetRejected} event with assetId = 0. + * @param tokenId ID of the token of which to clear the pending array. + * @param maxRejections Maximum number of expected assets to reject, used to prevent from rejecting assets which + * arrive just before this operation. + */ + function rejectAllAssets( + uint256 tokenId, + uint256 maxRejections + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _rejectAllAssets(tokenId, maxRejections); + } + + /** + * @notice Sets a new priority array for a given token. + * @dev The priority array is a non-sequential list of `uint16`s, where the lowest value is considered highest + * priority. + * @dev Value `0` of a priority is a special case equivalent to unitialized. + * @dev Requirements: + * + * - The caller must own the token or be approved to manage the token's assets + * - `tokenId` must exist. + * - The length of `priorities` must be equal the length of the active assets array. + * @dev Emits a {AssetPrioritySet} event. + * @param tokenId ID of the token to set the priorities for + * @param priorities An array of priority values + */ + function setPriority( + uint256 tokenId, + uint16[] calldata priorities + ) public virtual onlyApprovedForAssetsOrOwner(tokenId) { + _setPriority(tokenId, priorities); + } + + // --------------------- MULTIASSET INTERNAL --------------------- + + /** + * @notice Internal function for granting approvals for a specific token. + * @param to Address of the account we are granting an approval to + * @param tokenId ID of the token we are granting the approval for + */ + function _approveForAssets(address to, uint256 tokenId) internal virtual { + address owner = ownerOf(tokenId); + _tokenApprovalsForAssets[tokenId][owner] = to; + emit ApprovalForAssets(owner, to, tokenId); + } + + /** + * @notice Used to add a asset entry. + * @dev This internal function warrants custom access control to be implemented when used. + * @param id ID of the asset being added + * @param equippableGroupId ID of the equippable group being marked as equippable into the slot associated with + * `Parts` of the `Slot` type + * @param catalogAddress Address of the `Catalog` associated with the asset + * @param metadataURI The metadata URI of the asset + * @param partIds An array of IDs of fixed and slot parts to be included in the asset + */ + function _addAssetEntry( + uint64 id, + uint64 equippableGroupId, + address catalogAddress, + string memory metadataURI, + uint64[] calldata partIds + ) internal virtual { + _addAssetEntry(id, metadataURI); + + if (catalogAddress == address(0) && partIds.length != 0) + revert CatalogRequiredForParts(); + + _catalogAddresses[id] = catalogAddress; + _equippableGroupIds[id] = equippableGroupId; + _partIds[id] = partIds; + } + + /** + * @notice Used to accept a pending asset. + * @dev The call is reverted if there is no pending asset at a given index. + * @param tokenId ID of the token for which to accept the pending asset + * @param index Index of the asset in the pending array to accept + * @param assetId ID of the asset to accept in token's pending array + */ + function _acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual { + _validatePendingAssetAtIndex(tokenId, index, assetId); + _beforeAcceptAsset(tokenId, index, assetId); + + uint64 replacesId = _assetReplacements[tokenId][assetId]; + uint256 replaceIndex; + bool replacefound; + if (replacesId != uint64(0)) + (replaceIndex, replacefound) = _activeAssets[tokenId].indexOf( + replacesId + ); + + if (replacefound) { + // We don't want to remove and then push a new asset. + // This way we also keep the priority of the original asset + _activeAssets[tokenId][replaceIndex] = assetId; + delete _tokenAssets[tokenId][replacesId]; + } else { + // We use the current size as next priority, by default priorities would be [0,1,2...] + _activeAssetPriorities[tokenId].push( + uint16(_activeAssets[tokenId].length) + ); + _activeAssets[tokenId].push(assetId); + replacesId = uint64(0); + } + _removePendingAsset(tokenId, index, assetId); + + emit AssetAccepted(tokenId, assetId, replacesId); + _afterAcceptAsset(tokenId, index, assetId); + } + + /** + * @notice Used to reject the specified asset from the pending array. + * @dev The call is reverted if there is no pending asset at a given index. + * @param tokenId ID of the token that the asset is being rejected from + * @param index Index of the asset in the pending array to be rejected + * @param assetId ID of the asset expected to be in the index + */ + function _rejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) internal virtual { + _validatePendingAssetAtIndex(tokenId, index, assetId); + _beforeRejectAsset(tokenId, index, assetId); + + _removePendingAsset(tokenId, index, assetId); + delete _tokenAssets[tokenId][assetId]; + + emit AssetRejected(tokenId, assetId); + _afterRejectAsset(tokenId, index, assetId); + } + + /** + * @notice Used to validate the index on the pending assets array + * @dev The call is reverted if the index is out of range or the asset Id is not present at the index. + * @param tokenId ID of the token that the asset is validated from + * @param index Index of the asset in the pending array + * @param assetId Id of the asset expected to be in the index + */ + function _validatePendingAssetAtIndex( + uint256 tokenId, + uint256 index, + uint64 assetId + ) private view { + if (index >= _pendingAssets[tokenId].length) revert IndexOutOfRange(); + if (assetId != _pendingAssets[tokenId][index]) + revert UnexpectedAssetId(); + } + + /** + * @notice Used to remove the asset at the index on the pending assets array + * @param tokenId ID of the token that the asset is being removed from + * @param index Index of the asset in the pending array + * @param assetId Id of the asset expected to be in the index + */ + function _removePendingAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) private { + _pendingAssets[tokenId].removeItemByIndex(index); + delete _assetReplacements[tokenId][assetId]; + } + + /** + * @notice Used to reject all of the pending assets for the given token. + * @dev When rejecting all assets, the pending array is indiscriminately cleared. + * @dev If the number of pending assets is greater than the value of `maxRejections`, the exectuion will be + * reverted. + * @param tokenId ID of the token to reject all of the pending assets. + * @param maxRejections Maximum number of expected assets to reject, used to prevent from + * rejecting assets which arrive just before this operation. + */ + function _rejectAllAssets( + uint256 tokenId, + uint256 maxRejections + ) internal virtual { + uint256 len = _pendingAssets[tokenId].length; + if (len > maxRejections) revert UnexpectedNumberOfAssets(); + + _beforeRejectAllAssets(tokenId); + + for (uint256 i; i < len; ) { + uint64 assetId = _pendingAssets[tokenId][i]; + delete _assetReplacements[tokenId][assetId]; + unchecked { + ++i; + } + } + delete (_pendingAssets[tokenId]); + + emit AssetRejected(tokenId, uint64(0)); + _afterRejectAllAssets(tokenId); + } + + /** + * @notice Used to specify the priorities for a given token's active assets. + * @dev If the length of the priorities array doesn't match the length of the active assets array, the execution + * will be reverted. + * @dev The position of the priority value in the array corresponds the position of the asset in the active + * assets array it will be applied to. + * @param tokenId ID of the token for which the priorities are being set + * @param priorities Array of priorities for the assets + */ + function _setPriority( + uint256 tokenId, + uint16[] calldata priorities + ) internal virtual { + uint256 length = priorities.length; + if (length != _activeAssets[tokenId].length) + revert BadPriorityListLength(); + + _beforeSetPriority(tokenId, priorities); + _activeAssetPriorities[tokenId] = priorities; + + emit AssetPrioritySet(tokenId); + _afterSetPriority(tokenId, priorities); + } + + /** + * @notice Used to add an asset entry. + * @dev If the specified ID is already used by another asset, the execution will be reverted. + * @dev This internal function warrants custom access control to be implemented when used. + * @param id ID of the asset to assign to the new asset + * @param metadataURI Metadata URI of the asset + */ + function _addAssetEntry( + uint64 id, + string memory metadataURI + ) internal virtual { + if (id == uint64(0)) revert IdZeroForbidden(); + if (bytes(_assets[id]).length > 0) revert AssetAlreadyExists(); + + _beforeAddAsset(id, metadataURI); + _assets[id] = metadataURI; + + emit AssetSet(id); + _afterAddAsset(id, metadataURI); + } + + /** + * @notice Used to add an asset to a token. + * @dev If the given asset is already added to the token, the execution will be reverted. + * @dev If the asset ID is invalid, the execution will be reverted. + * @dev If the token already has the maximum amount of pending assets (128), the execution will be + * reverted. + * @param tokenId ID of the token to add the asset to + * @param assetId ID of the asset to add to the token + * @param replacesAssetWithId ID of the asset to replace from the token's list of active assets + */ + function _addAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual { + if (_tokenAssets[tokenId][assetId]) revert AssetAlreadyExists(); + + if (bytes(_assets[assetId]).length == 0) revert NoAssetMatchingId(); + + if (_pendingAssets[tokenId].length >= 128) + revert MaxPendingAssetsReached(); + + _beforeAddAssetToToken(tokenId, assetId, replacesAssetWithId); + _tokenAssets[tokenId][assetId] = true; + _pendingAssets[tokenId].push(assetId); + + if (replacesAssetWithId != uint64(0)) { + _assetReplacements[tokenId][assetId] = replacesAssetWithId; + } + + emit AssetAddedToToken(tokenId, assetId, replacesAssetWithId); + _afterAddAssetToToken(tokenId, assetId, replacesAssetWithId); + } + + // --------------------- EQUIPPABLE GETTERS --------------------- + + /** + * @inheritdoc IEquippable + */ + function canTokenBeEquippedWithAssetIntoSlot( + address parent, + uint256 tokenId, + uint64 assetId, + uint64 slotId + ) public view virtual returns (bool) { + uint64 equippableGroupId = _equippableGroupIds[assetId]; + uint64 equippableSlot = _validParentSlots[equippableGroupId][parent]; + if (equippableSlot == slotId) { + (, bool found) = getActiveAssets(tokenId).indexOf(assetId); + return found; + } + return false; + } + + /** + * @inheritdoc IEquippable + */ + function isChildEquipped( + uint256 tokenId, + address childAddress, + uint256 childId + ) public view virtual returns (bool) { + return _equipCountPerChild[tokenId][childAddress][childId] != uint8(0); + } + + /** + * @inheritdoc IEquippable + */ + function getAssetAndEquippableData( + uint256 tokenId, + uint64 assetId + ) + public + view + virtual + returns (string memory, uint64, address, uint64[] memory) + { + return ( + getAssetMetadata(tokenId, assetId), + _equippableGroupIds[assetId], + _catalogAddresses[assetId], + _partIds[assetId] + ); + } + + /** + * @inheritdoc IEquippable + */ + function getEquipment( + uint256 tokenId, + address targetCatalogAddress, + uint64 slotPartId + ) public view virtual returns (Equipment memory) { + return _equipments[tokenId][targetCatalogAddress][slotPartId]; + } + + // --------------------- EQUIPPABLE SETTERS --------------------- + + /** + * @inheritdoc IEquippable + */ + function equip( + IntakeEquip memory data + ) public virtual onlyApprovedOrOwner(data.tokenId) nonReentrant { + _equip(data); + } + + /** + * @inheritdoc IEquippable + */ + function unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) public virtual onlyApprovedOrOwner(tokenId) { + _unequip(tokenId, assetId, slotPartId); + } + + // --------------------- EQUIPPABLE INTERNAL --------------------- + + /** + * @notice Private function used to equip a child into a token. + * @dev If the `Slot` already has an item equipped, the execution will be reverted. + * @dev If the child can't be used in the given `Slot`, the execution will be reverted. + * @dev If the catalog doesn't allow this equip to happen, the execution will be reverted. + * @dev The `IntakeEquip` stuct contains the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data An `IntakeEquip` struct specifying the equip data + */ + function _equip(IntakeEquip memory data) internal virtual { + address catalogAddress = _catalogAddresses[data.assetId]; + uint64 slotPartId = data.slotPartId; + if ( + _equipments[data.tokenId][catalogAddress][slotPartId] + .childEquippableAddress != address(0) + ) revert SlotAlreadyUsed(); + + // Check from parent's asset perspective: + (, bool found) = _partIds[data.assetId].indexOf(slotPartId); + if (!found) revert TargetAssetCannotReceiveSlot(); + + IERC6059.Child memory child = childOf(data.tokenId, data.childIndex); + + // Check from child perspective intention to be used in part + // We add reentrancy guard because of this call, it happens before updating state + if ( + !IEquippable(child.contractAddress) + .canTokenBeEquippedWithAssetIntoSlot( + address(this), + child.tokenId, + data.childAssetId, + slotPartId + ) + ) revert TokenCannotBeEquippedWithAssetIntoSlot(); + + // Check from catalog perspective + if ( + !ICatalog(catalogAddress).checkIsEquippable( + slotPartId, + child.contractAddress + ) + ) revert EquippableEquipNotAllowedByCatalog(); + + _beforeEquip(data); + Equipment memory newEquip = Equipment({ + assetId: data.assetId, + childAssetId: data.childAssetId, + childId: child.tokenId, + childEquippableAddress: child.contractAddress + }); + + _equipments[data.tokenId][catalogAddress][slotPartId] = newEquip; + _equipCountPerChild[data.tokenId][child.contractAddress][ + child.tokenId + ] += 1; + + emit ChildAssetEquipped( + data.tokenId, + data.assetId, + slotPartId, + child.tokenId, + child.contractAddress, + data.childAssetId + ); + _afterEquip(data); + } + + /** + * @notice Private function used to unequip child from parent token. + * @param tokenId ID of the parent from which the child is being unequipped + * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped + * @param slotPartId ID of the `Slot` from which to unequip the child + */ + function _unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) internal virtual { + address targetCatalogAddress = _catalogAddresses[assetId]; + Equipment memory equipment = _equipments[tokenId][targetCatalogAddress][ + slotPartId + ]; + if (equipment.childEquippableAddress == address(0)) + revert NotEquipped(); + _beforeUnequip(tokenId, assetId, slotPartId); + + delete _equipments[tokenId][targetCatalogAddress][slotPartId]; + _equipCountPerChild[tokenId][equipment.childEquippableAddress][ + equipment.childId + ] -= 1; + + emit ChildAssetUnequipped( + tokenId, + assetId, + slotPartId, + equipment.childId, + equipment.childEquippableAddress, + equipment.childAssetId + ); + _afterUnequip(tokenId, assetId, slotPartId); + } + + /** + * @notice Internal function used to declare that the assets belonging to a given `equippableGroupId` are + * equippable into the `Slot` associated with the `partId` of the collection at the specified `parentAddress` + * @param equippableGroupId ID of the equippable group + * @param parentAddress Address of the parent into which the equippable group can be equipped into + * @param slotPartId ID of the `Slot` that the items belonging to the equippable group can be equipped into + */ + function _setValidParentForEquippableGroup( + uint64 equippableGroupId, + address parentAddress, + uint64 slotPartId + ) internal virtual { + if (equippableGroupId == uint64(0) || slotPartId == uint64(0)) + revert IdZeroForbidden(); + _validParentSlots[equippableGroupId][parentAddress] = slotPartId; + emit ValidParentEquippableGroupIdSet( + equippableGroupId, + slotPartId, + parentAddress + ); + } + + // --------------------- MODIFIERS IMPLEMENTATIONS --------------------- + + /** + * @notice Used to verify that the caller is either the owner of the token or approved to manage it by its owner. + * @dev If the caller is not the owner of the token or approved to manage it by its owner, the execution will be + * reverted. + * @param tokenId ID of the token to check + */ + function _onlyApprovedOrOwner(uint256 tokenId) private view { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) + revert ERC721NotApprovedOrOwner(); + } + + /** + * @notice Used to verify that the caller is approved to manage the given token or it its direct owner. + * @dev This does not delegate to ownerOf, which returns the root owner, but rater uses an owner from DirectOwner + * struct. + * @dev The execution is reverted if the caller is not immediate owner or approved to manage the given token. + * @dev Used for parent-scoped transfers. + * @param tokenId ID of the token to check. + */ + function _onlyApprovedOrDirectOwner(uint256 tokenId) private view { + if (!_isApprovedOrDirectOwner(_msgSender(), tokenId)) + revert NotApprovedOrDirectOwner(); + } + + /** + * @notice Used to verify that the caller is either the owner of the given token or approved to manage the token's assets + * of the owner. + * @param tokenId ID of the token that we are checking + */ + function _onlyApprovedForAssetsOrOwner(uint256 tokenId) private view { + if (!_isApprovedForAssetsOrOwner(_msgSender(), tokenId)) + revert NotApprovedForAssetsOrOwner(); + } + + /** + * @notice Used to check whether the given account is allowed to manage the given token. + * @dev Requirements: + * + * - `tokenId` must exist. + * @param spender Address that is being checked for approval + * @param tokenId ID of the token being checked + * @return bool The boolean value indicating whether the `spender` is approved to manage the given token + */ + function _isApprovedOrOwner( + address spender, + uint256 tokenId + ) internal view virtual returns (bool) { + address owner = ownerOf(tokenId); + return (spender == owner || + isApprovedForAll(owner, spender) || + getApproved(tokenId) == spender); + } + + /** + * @notice Used to check whether the account is approved to manage the token or its direct owner. + * @param spender Address that is being checked for approval or direct ownership + * @param tokenId ID of the token being checked + * @return bool The boolean value indicating whether the `spender` is approved to manage the given token or its + * direct owner + */ + function _isApprovedOrDirectOwner( + address spender, + uint256 tokenId + ) internal view virtual returns (bool) { + (address owner, uint256 parentId, ) = directOwnerOf(tokenId); + // When the parent is an NFT, only it can do operations + if (parentId != 0) { + return (spender == owner); + } + // Otherwise, the owner or approved address can + return (spender == owner || + isApprovedForAll(owner, spender) || + getApproved(tokenId) == spender); + } + + /** + * @notice Internal function to check whether the queried user is either: + * 1. The root owner of the token associated with `tokenId`. + * 2. Is approved for all assets of the current owner via the `setApprovalForAllForAssets` function. + * 3. Is granted approval for the specific tokenId for asset management via the `approveForAssets` function. + * @param user Address of the user we are checking for permission + * @param tokenId ID of the token to query for permission for a given `user` + * @return bool A boolean value indicating whether the user is approved to manage the token or not + */ + function _isApprovedForAssetsOrOwner( + address user, + uint256 tokenId + ) internal view virtual returns (bool) { + address owner = ownerOf(tokenId); + return (user == owner || + isApprovedForAllForAssets(owner, user) || + getApprovedForAssets(tokenId) == user); + } + + // --------------------- MULTIASSET HOOKS --------------------- + + /** + * @notice Hook that is called before an asset is added. + * @param id ID of the asset + * @param metadataURI Metadata URI of the asset + */ + function _beforeAddAsset( + uint64 id, + string memory metadataURI + ) internal virtual {} + + /** + * @notice Hook that is called after an asset is added. + * @param id ID of the asset + * @param metadataURI Metadata URI of the asset + */ + function _afterAddAsset( + uint64 id, + string memory metadataURI + ) internal virtual {} + + /** + * @notice Hook that is called before adding an asset to a token's pending assets array. + * @dev If the asset doesn't intend to replace another asset, the `replacesAssetWithId` value should be `0`. + * @param tokenId ID of the token to which the asset is being added + * @param assetId ID of the asset that is being added + * @param replacesAssetWithId ID of the asset that this asset is attempting to replace + */ + function _beforeAddAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual {} + + /** + * @notice Hook that is called after an asset has been added to a token's pending assets array. + * @dev If the asset doesn't intend to replace another asset, the `replacesAssetWithId` value should be `0`. + * @param tokenId ID of the token to which the asset is has been added + * @param assetId ID of the asset that is has been added + * @param replacesAssetWithId ID of the asset that this asset is attempting to replace + */ + function _afterAddAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) internal virtual {} + + /** + * @notice Hook that is called before an asset is accepted to a token's active assets array. + * @param tokenId ID of the token for which the asset is being accepted + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to be located at the specified `index` + */ + function _beforeAcceptAsset( + uint256 tokenId, + uint256 index, + uint256 assetId + ) internal virtual {} + + /** + * @notice Hook that is called after an asset is accepted to a token's active assets array. + * @param tokenId ID of the token for which the asset has been accepted + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to have been located at the specified `index` + */ + function _afterAcceptAsset( + uint256 tokenId, + uint256 index, + uint256 assetId + ) internal virtual {} + + /** + * @notice Hook that is called before rejecting an asset. + * @param tokenId ID of the token from which the asset is being rejected + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to be located at the specified `index` + */ + function _beforeRejectAsset( + uint256 tokenId, + uint256 index, + uint256 assetId + ) internal virtual {} + + /** + * @notice Hook that is called after rejecting an asset. + * @param tokenId ID of the token from which the asset has been rejected + * @param index Index of the asset in the token's pending assets array + * @param assetId ID of the asset expected to have been located at the specified `index` + */ + function _afterRejectAsset( + uint256 tokenId, + uint256 index, + uint256 assetId + ) internal virtual {} + + /** + * @notice Hook that is called before rejecting all assets of a token. + * @param tokenId ID of the token from which all of the assets are being rejected + */ + function _beforeRejectAllAssets(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called after rejecting all assets of a token. + * @param tokenId ID of the token from which all of the assets have been rejected + */ + function _afterRejectAllAssets(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called before the priorities for token's assets is set. + * @param tokenId ID of the token for which the asset priorities are being set + * @param priorities[] An array of priorities for token's active resources + */ + function _beforeSetPriority( + uint256 tokenId, + uint16[] calldata priorities + ) internal virtual {} + + /** + * @notice Hook that is called after the priorities for token's assets is set. + * @param tokenId ID of the token for which the asset priorities have been set + * @param priorities[] An array of priorities for token's active resources + */ + function _afterSetPriority( + uint256 tokenId, + uint16[] calldata priorities + ) internal virtual {} + + // --------------------- NESTABLE HOOKS --------------------- + + /** + * @notice Hook that is called before any token transfer. This includes minting and burning. + * @dev Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be transferred to `to`. + * - When `from` is zero, `tokenId` will be minted to `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` and `to` are never zero at the same time. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token is being transferred + * @param to Address to which the token is being transferred + * @param tokenId ID of the token being transferred + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called after any transfer of tokens. This includes minting and burning. + * @dev Calling conditions: + * + * - When `from` and `to` are both non-zero. + * - `from` and `to` are never zero at the same time. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token has been transferred + * @param to Address to which the token has been transferred + * @param tokenId ID of the token that has been transferred + */ + function _afterTokenTransfer( + address from, + address to, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called before nested token transfer. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token is being transferred + * @param to Address to which the token is being transferred + * @param fromTokenId ID of the token from which the given token is being transferred + * @param toTokenId ID of the token to which the given token is being transferred + * @param tokenId ID of the token being transferred + */ + function _beforeNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called after nested token transfer. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param from Address from which the token was transferred + * @param to Address to which the token was transferred + * @param fromTokenId ID of the token from which the given token was transferred + * @param toTokenId ID of the token to which the given token was transferred + * @param tokenId ID of the token that was transferred + */ + function _afterNestedTokenTransfer( + address from, + address to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 tokenId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is added to the pending tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will receive a new pending child token + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _beforeAddChild( + uint256 tokenId, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called after a child is added to the pending tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has received a new pending child token + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _afterAddChild( + uint256 tokenId, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is accepted to the active tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param parentId ID of the token that will accept a pending child token + * @param childIndex Index of the child token to accept in the given parent token's pending children array + * @param childAddress Address of the collection smart contract of the child token expected to be located at the + * specified index of the given parent token's pending children array + * @param childId ID of the child token expected to be located at the specified index of the given parent token's + * pending children array + */ + function _beforeAcceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called after a child is accepted to the active tokens array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param parentId ID of the token that has accepted a pending child token + * @param childIndex Index of the child token that was accpeted in the given parent token's pending children array + * @param childAddress Address of the collection smart contract of the child token that was expected to be located + * at the specified index of the given parent token's pending children array + * @param childId ID of the child token that was expected to be located at the specified index of the given parent + * token's pending children array + */ + function _afterAcceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) internal virtual {} + + /** + * @notice Hook that is called before a child is transferred from a given child token array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will transfer a child token + * @param childIndex Index of the child token that will be transferred from the given parent token's children array + * @param childAddress Address of the collection smart contract of the child token that is expected to be located + * at the specified index of the given parent token's children array + * @param childId ID of the child token that is expected to be located at the specified index of the given parent + * token's children array + * @param isPending A boolean value signifying whether the child token is being transferred from the pending child + * tokens array (`true`) or from the active child tokens array (`false`) + */ + function _beforeTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending + ) internal virtual {} + + /** + * @notice Hook that is called after a child is transferred from a given child token array of a given token. + * @dev The Child struct consists of the following values: + * [ + * tokenId, + * contractAddress + * ] + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has transferred a child token + * @param childIndex Index of the child token that was transferred from the given parent token's children array + * @param childAddress Address of the collection smart contract of the child token that was expected to be located + * at the specified index of the given parent token's children array + * @param childId ID of the child token that was expected to be located at the specified index of the given parent + * token's children array + * @param isPending A boolean value signifying whether the child token was transferred from the pending child tokens + * array (`true`) or from the active child tokens array (`false`) + */ + function _afterTransferChild( + uint256 tokenId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending + ) internal virtual {} + + /** + * @notice Hook that is called before a pending child tokens array of a given token is cleared. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that will reject all of the pending child tokens + */ + function _beforeRejectAllChildren(uint256 tokenId) internal virtual {} + + /** + * @notice Hook that is called after a pending child tokens array of a given token is cleared. + * @dev To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + * @param tokenId ID of the token that has rejected all of the pending child tokens + */ + function _afterRejectAllChildren(uint256 tokenId) internal virtual {} + + // --------------------- EQUIPPABLE HOOKS --------------------- + + /** + * @notice A hook to be called before a equipping a asset to the token. + * @dev The `IntakeEquip` struct consist of the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data The `IntakeEquip` struct containing data of the asset that is being equipped + */ + function _beforeEquip(IntakeEquip memory data) internal virtual {} + + /** + * @notice A hook to be called after equipping a asset to the token. + * @dev The `IntakeEquip` struct consist of the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data The `IntakeEquip` struct containing data of the asset that was equipped + */ + function _afterEquip(IntakeEquip memory data) internal virtual {} + + /** + * @notice A hook to be called before unequipping a asset from the token. + * @param tokenId ID of the token from which the asset is being unequipped + * @param assetId ID of the asset being unequipped + * @param slotPartId ID of the slot from which the asset is being unequipped + */ + function _beforeUnequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) internal virtual {} + + /** + * @notice A hook to be called after unequipping a asset from the token. + * @param tokenId ID of the token from which the asset was unequipped + * @param assetId ID of the asset that was unequipped + * @param slotPartId ID of the slot from which the asset was unequipped + */ + function _afterUnequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) internal virtual {} +} diff --git a/assets/eip-6220/contracts/ICatalog.sol b/assets/eip-6220/contracts/ICatalog.sol new file mode 100644 index 00000000000000..ba91ebb7584fc1 --- /dev/null +++ b/assets/eip-6220/contracts/ICatalog.sol @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; + +/** + * @title ICatalog + * @author RMRK team + * @notice An interface Catalog for equippable module. + */ +interface ICatalog is IERC165 { + /** + * @notice Event to announce addition of a new part. + * @dev It is emitted when a new part is added. + * @param partId ID of the part that was added + * @param itemType Enum value specifying whether the part is `None`, `Slot` and `Fixed` + * @param zIndex An uint specifying the z value of the part. It is used to specify the depth which the part should + * be rendered at + * @param equippableAddresses An array of addresses that can equip this part + * @param metadataURI The metadata URI of the part + */ + event AddedPart( + uint64 indexed partId, + ItemType indexed itemType, + uint8 zIndex, + address[] equippableAddresses, + string metadataURI + ); + + /** + * @notice Event to announce new equippables to the part. + * @dev It is emitted when new addresses are marked as equippable for `partId`. + * @param partId ID of the part that had new equippable addresses added + * @param equippableAddresses An array of the new addresses that can equip this part + */ + event AddedEquippables( + uint64 indexed partId, + address[] equippableAddresses + ); + + /** + * @notice Event to announce the overriding of equippable addresses of the part. + * @dev It is emitted when the existing list of addresses marked as equippable for `partId` is overwritten by a new + * one. + * @param partId ID of the part whose list of equippable addresses was overwritten + * @param equippableAddresses The new, full, list of addresses that can equip this part + */ + event SetEquippables(uint64 indexed partId, address[] equippableAddresses); + + /** + * @notice Event to announce that a given part can be equipped by any address. + * @dev It is emitted when a given part is marked as equippable by any. + * @param partId ID of the part marked as equippable by any address + */ + event SetEquippableToAll(uint64 indexed partId); + + /** + * @notice Used to define a type of the item. Possible values are `None`, `Slot` or `Fixed`. + * @dev Used for fixed and slot parts. + */ + enum ItemType { + None, + Slot, + Fixed + } + + /** + * @notice The integral structure of a standard RMRK catalog item defining it. + * @dev Requires a minimum of 3 storage slots per catalog item, equivalent to roughly 60,000 gas as of Berlin hard fork + * (April 14, 2021), though 5-7 storage slots is more realistic, given the standard length of an IPFS URI. This + * will result in between 25,000,000 and 35,000,000 gas per 250 assets--the maximum block size of Ethereum + * mainnet is 30M at peak usage. + * @return itemType The item type of the part + * @return z The z value of the part defining how it should be rendered when presenting the full NFT + * @return equippable The array of addresses allowed to be equipped in this part + * @return metadataURI The metadata URI of the part + */ + struct Part { + ItemType itemType; //1 byte + uint8 z; //1 byte + address[] equippable; //n Collections that can be equipped into this slot + string metadataURI; //n bytes 32+ + } + + /** + * @notice The structure used to add a new `Part`. + * @dev The part is added with specified ID, so you have to make sure that you are using an unused `partId`, + * otherwise the addition of the part vill be reverted. + * @dev The full `IntakeStruct` looks like this: + * [ + * partID, + * [ + * itemType, + * z, + * [ + * permittedCollectionAddress0, + * permittedCollectionAddress1, + * permittedCollectionAddress2 + * ], + * metadataURI + * ] + * ] + * @return partId ID to be assigned to the `Part` + * @return part A `Part` to be added + */ + struct IntakeStruct { + uint64 partId; + Part part; + } + + /** + * @notice Used to return the metadata URI of the associated catalog. + * @return string Base metadata URI + */ + function getMetadataURI() external view returns (string memory); + + /** + * @notice Used to return the `itemType` of the associated catalog + * @return string `itemType` of the associated catalog + */ + function getType() external view returns (string memory); + + /** + * @notice Used to check whether the given address is allowed to equip the desired `Part`. + * @dev Returns true if a collection may equip asset with `partId`. + * @param partId The ID of the part that we are checking + * @param targetAddress The address that we are checking for whether the part can be equipped into it or not + * @return bool The status indicating whether the `targetAddress` can be equipped into `Part` with `partId` or not + */ + function checkIsEquippable(uint64 partId, address targetAddress) + external + view + returns (bool); + + /** + * @notice Used to check if the part is equippable by all addresses. + * @dev Returns true if part is equippable to all. + * @param partId ID of the part that we are checking + * @return bool The status indicating whether the part with `partId` can be equipped by any address or not + */ + function checkIsEquippableToAll(uint64 partId) external view returns (bool); + + /** + * @notice Used to retrieve a `Part` with id `partId` + * @param partId ID of the part that we are retrieving + * @return struct The `Part` struct associated with given `partId` + */ + function getPart(uint64 partId) external view returns (Part memory); + + /** + * @notice Used to retrieve multiple parts at the same time. + * @param partIds An array of part IDs that we want to retrieve + * @return struct An array of `Part` structs associated with given `partIds` + */ + function getParts(uint64[] calldata partIds) + external + view + returns (Part[] memory); +} diff --git a/assets/eip-6220/contracts/IERC5773.sol b/assets/eip-6220/contracts/IERC5773.sol new file mode 100644 index 00000000000000..de7515c088c1ad --- /dev/null +++ b/assets/eip-6220/contracts/IERC5773.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +interface IERC5773 { + event AssetSet(uint64 assetId); + + event AssetAddedToToken( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed replacesId + ); + + event AssetAccepted( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed replacesId + ); + + event AssetRejected(uint256 indexed tokenId, uint64 indexed assetId); + + event AssetPrioritySet(uint256 indexed tokenId); + + event ApprovalForAssets( + address indexed owner, + address indexed approved, + uint256 indexed tokenId + ); + + event ApprovalForAllForAssets( + address indexed owner, + address indexed operator, + bool approved + ); + + function acceptAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) external; + + function rejectAsset( + uint256 tokenId, + uint256 index, + uint64 assetId + ) external; + + function rejectAllAssets(uint256 tokenId, uint256 maxRejections) external; + + function setPriority( + uint256 tokenId, + uint16[] calldata priorities + ) external; + + function getActiveAssets( + uint256 tokenId + ) external view returns (uint64[] memory); + + function getPendingAssets( + uint256 tokenId + ) external view returns (uint64[] memory); + + function getActiveAssetPriorities( + uint256 tokenId + ) external view returns (uint16[] memory); + + function getAssetReplacements( + uint256 tokenId, + uint64 newAssetId + ) external view returns (uint64); + + function getAssetMetadata( + uint256 tokenId, + uint64 assetId + ) external view returns (string memory); + + function approveForAssets(address to, uint256 tokenId) external; + + function getApprovedForAssets( + uint256 tokenId + ) external view returns (address); + + function setApprovalForAllForAssets( + address operator, + bool approved + ) external; + + function isApprovedForAllForAssets( + address owner, + address operator + ) external view returns (bool); +} diff --git a/assets/eip-6220/contracts/IERC6059.sol b/assets/eip-6220/contracts/IERC6059.sol new file mode 100644 index 00000000000000..dbad8969f5f4df --- /dev/null +++ b/assets/eip-6220/contracts/IERC6059.sol @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +interface IERC6059 { + struct DirectOwner { + uint256 tokenId; + address ownerAddress; + bool isNft; + } + + event NestTransfer( + address indexed from, + address indexed to, + uint256 fromTokenId, + uint256 toTokenId, + uint256 indexed tokenId + ); + + event ChildProposed( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + event ChildAccepted( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId + ); + + event AllChildrenRejected(uint256 indexed tokenId); + + event ChildTransferred( + uint256 indexed tokenId, + uint256 childIndex, + address indexed childAddress, + uint256 indexed childId, + bool fromPending + ); + + struct Child { + uint256 tokenId; + address contractAddress; + } + + function ownerOf(uint256 tokenId) external view returns (address owner); + + function directOwnerOf( + uint256 tokenId + ) external view returns (address, uint256, bool); + + function burn( + uint256 tokenId, + uint256 maxRecursiveBurns + ) external returns (uint256); + + function addChild( + uint256 parentId, + uint256 childId, + bytes memory data + ) external; + + function acceptChild( + uint256 parentId, + uint256 childIndex, + address childAddress, + uint256 childId + ) external; + + function rejectAllChildren( + uint256 parentId, + uint256 maxRejections + ) external; + + function transferChild( + uint256 tokenId, + address to, + uint256 destinationId, + uint256 childIndex, + address childAddress, + uint256 childId, + bool isPending, + bytes memory data + ) external; + + function childrenOf( + uint256 parentId + ) external view returns (Child[] memory); + + function pendingChildrenOf( + uint256 parentId + ) external view returns (Child[] memory); + + function childOf( + uint256 parentId, + uint256 index + ) external view returns (Child memory); + + function pendingChildOf( + uint256 parentId, + uint256 index + ) external view returns (Child memory); + + function nestTransferFrom( + address from, + address to, + uint256 tokenId, + uint256 destinationId, + bytes memory data + ) external; +} diff --git a/assets/eip-6220/contracts/IEquippable.sol b/assets/eip-6220/contracts/IEquippable.sol new file mode 100644 index 00000000000000..72edc119dadd3b --- /dev/null +++ b/assets/eip-6220/contracts/IEquippable.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "./IERC5773.sol"; + +/** + * @title IEquippable + * @author RMRK team + * @notice Interface smart contract of the equippable module. + */ +interface IEquippable is IERC5773 { + /** + * @notice Used to store the core structure of the `Equippable` component. + * @return assetId The ID of the asset equipping a child + * @return childAssetId The ID of the asset used as equipment + * @return childId The ID of token that is equipped + * @return childEquippableAddress Address of the collection to which the child asset belongs to + */ + struct Equipment { + uint64 assetId; + uint64 childAssetId; + uint256 childId; + address childEquippableAddress; + } + + /** + * @notice Used to provide a struct for inputing equip data. + * @dev Only used for input and not storage of data. + * @return tokenId ID of the token we are managing + * @return childIndex Index of a child in the list of token's active children + * @return assetId ID of the asset that we are equipping into + * @return slotPartId ID of the slot part that we are using to equip + * @return childAssetId ID of the asset that we are equipping + */ + struct IntakeEquip { + uint256 tokenId; + uint256 childIndex; + uint64 assetId; + uint64 slotPartId; + uint64 childAssetId; + } + + /** + * @notice Used to notify listeners that a child's asset has been equipped into one of its parent assets. + * @param tokenId ID of the token that had an asset equipped + * @param assetId ID of the asset associated with the token we are equipping into + * @param slotPartId ID of the slot we are using to equip + * @param childId ID of the child token we are equipping into the slot + * @param childAddress Address of the child token's collection + * @param childAssetId ID of the asset associated with the token we are equipping + */ + event ChildAssetEquipped( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed slotPartId, + uint256 childId, + address childAddress, + uint64 childAssetId + ); + + /** + * @notice Used to notify listeners that a child's asset has been unequipped from one of its parent assets. + * @param tokenId ID of the token that had an asset unequipped + * @param assetId ID of the asset associated with the token we are unequipping out of + * @param slotPartId ID of the slot we are unequipping from + * @param childId ID of the token being unequipped + * @param childAddress Address of the collection that a token that is being unequipped belongs to + * @param childAssetId ID of the asset associated with the token we are unequipping + */ + event ChildAssetUnequipped( + uint256 indexed tokenId, + uint64 indexed assetId, + uint64 indexed slotPartId, + uint256 childId, + address childAddress, + uint64 childAssetId + ); + + /** + * @notice Used to notify listeners that the assets belonging to a `equippableGroupId` have been marked as + * equippable into a given slot and parent + * @param equippableGroupId ID of the equippable group being marked as equippable into the slot associated with + * `slotPartId` of the `parentAddress` collection + * @param slotPartId ID of the slot part of the catalog into which the parts belonging to the equippable group + * associated with `equippableGroupId` can be equipped + * @param parentAddress Address of the collection into which the parts belonging to `equippableGroupId` can be + * equipped + */ + event ValidParentEquippableGroupIdSet( + uint64 indexed equippableGroupId, + uint64 indexed slotPartId, + address parentAddress + ); + + /** + * @notice Used to equip a child into a token. + * @dev The `IntakeEquip` stuct contains the following data: + * [ + * tokenId, + * childIndex, + * assetId, + * slotPartId, + * childAssetId + * ] + * @param data An `IntakeEquip` struct specifying the equip data + */ + function equip( + IntakeEquip memory data + ) external; + + /** + * @notice Used to unequip child from parent token. + * @dev This can only be called by the owner of the token or by an account that has been granted permission to + * manage the given token by the current owner. + * @param tokenId ID of the parent from which the child is being unequipped + * @param assetId ID of the parent's asset that contains the `Slot` into which the child is equipped + * @param slotPartId ID of the `Slot` from which to unequip the child + */ + function unequip( + uint256 tokenId, + uint64 assetId, + uint64 slotPartId + ) external; + + /** + * @notice Used to check whether the token has a given child equipped. + * @dev This is used to prevent from transferring a child that is equipped. + * @param tokenId ID of the parent token for which we are querying for + * @param childAddress Address of the child token's smart contract + * @param childId ID of the child token + * @return bool The boolean value indicating whether the child token is equipped into the given token or not + */ + function isChildEquipped( + uint256 tokenId, + address childAddress, + uint256 childId + ) external view returns (bool); + + /** + * @notice Used to verify whether a token can be equipped into a given parent's slot. + * @param parent Address of the parent token's smart contract + * @param tokenId ID of the token we want to equip + * @param assetId ID of the asset associated with the token we want to equip + * @param slotId ID of the slot that we want to equip the token into + * @return bool The boolean indicating whether the token with the given asset can be equipped into the desired + * slot + */ + function canTokenBeEquippedWithAssetIntoSlot( + address parent, + uint256 tokenId, + uint64 assetId, + uint64 slotId + ) external view returns (bool); + + /** + * @notice Used to get the Equipment object equipped into the specified slot of the desired token. + * @dev The `Equipment` struct consists of the following data: + * [ + * assetId, + * childAssetId, + * childId, + * childEquippableAddress + * ] + * @param tokenId ID of the token for which we are retrieving the equipped object + * @param targetCatalogAddress Address of the `Catalog` associated with the `Slot` part of the token + * @param slotPartId ID of the `Slot` part that we are checking for equipped objects + * @return struct The `Equipment` struct containing data about the equipped object + */ + function getEquipment( + uint256 tokenId, + address targetCatalogAddress, + uint64 slotPartId + ) external view returns (Equipment memory); + + /** + * @notice Used to get the asset and equippable data associated with given `assetId`. + * @param tokenId ID of the token for which to retrieve the asset + * @param assetId ID of the asset of which we are retrieving + * @return metadataURI The metadata URI of the asset + * @return equippableGroupId ID of the equippable group this asset belongs to + * @return catalogAddress The address of the catalog the part belongs to + * @return partIds An array of IDs of parts included in the asset + */ + function getAssetAndEquippableData(uint256 tokenId, uint64 assetId) + external + view + returns ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] calldata partIds + ); +} diff --git a/assets/eip-6220/contracts/library/EquippableLib.sol b/assets/eip-6220/contracts/library/EquippableLib.sol new file mode 100644 index 00000000000000..1ae233ab71b1ae --- /dev/null +++ b/assets/eip-6220/contracts/library/EquippableLib.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.0; + +library EquippableLib { + function indexOf( + uint64[] memory A, + uint64 a + ) internal pure returns (uint256, bool) { + uint256 length = A.length; + for (uint256 i; i < length; ) { + if (A[i] == a) { + return (i, true); + } + unchecked { + ++i; + } + } + return (0, false); + } + + //For reasource storage array + function removeItemByIndex(uint64[] storage array, uint256 index) internal { + //Check to see if this is already gated by require in all calls + require(index < array.length); + array[index] = array[array.length - 1]; + array.pop(); + } +} diff --git a/assets/eip-6220/contracts/mocks/CatalogMock.sol b/assets/eip-6220/contracts/mocks/CatalogMock.sol new file mode 100644 index 00000000000000..106762aaf3c5e2 --- /dev/null +++ b/assets/eip-6220/contracts/mocks/CatalogMock.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "../Catalog.sol"; + +contract CatalogMock is Catalog { + constructor(string memory metadataURI, string memory type_) + Catalog(metadataURI, type_) + {} + + function addPart(IntakeStruct calldata intakeStruct) external { + _addPart(intakeStruct); + } + + function addPartList(IntakeStruct[] calldata intakeStructs) external { + _addPartList(intakeStructs); + } + + function addEquippableAddresses( + uint64 partId, + address[] calldata equippableAddresses + ) external { + _addEquippableAddresses(partId, equippableAddresses); + } + + function setEquippableAddresses( + uint64 partId, + address[] calldata equippableAddresses + ) external { + _setEquippableAddresses(partId, equippableAddresses); + } + + function setEquippableToAll(uint64 partId) external { + _setEquippableToAll(partId); + } + + function resetEquippableAddresses(uint64 partId) external { + _resetEquippableAddresses(partId); + } +} diff --git a/assets/eip-6220/contracts/mocks/ERC721Mock.sol b/assets/eip-6220/contracts/mocks/ERC721Mock.sol new file mode 100644 index 00000000000000..995868858e680b --- /dev/null +++ b/assets/eip-6220/contracts/mocks/ERC721Mock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +/** + * @title ERC721Mock + * Used for tests with non ERC721 implementer + */ +contract ERC721Mock is ERC721 { + constructor( + string memory name, + string memory symbol + ) ERC721(name, symbol) {} +} diff --git a/assets/eip-6220/contracts/mocks/ERC721ReceiverMock.sol b/assets/eip-6220/contracts/mocks/ERC721ReceiverMock.sol new file mode 100644 index 00000000000000..1f0665b8c304ca --- /dev/null +++ b/assets/eip-6220/contracts/mocks/ERC721ReceiverMock.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.15; + +contract ERC721ReceiverMock { + bytes4 constant ERC721_RECEIVED = 0x150b7a02; + + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) public returns (bytes4) { + return ERC721_RECEIVED; + } +} diff --git a/assets/eip-6220/contracts/mocks/EquippableTokenMock.sol b/assets/eip-6220/contracts/mocks/EquippableTokenMock.sol new file mode 100644 index 00000000000000..243be8340f6b90 --- /dev/null +++ b/assets/eip-6220/contracts/mocks/EquippableTokenMock.sol @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "../EquippableToken.sol"; + +//Minimal public implementation of INestable for testing. +contract EquippableTokenMock is EquippableToken { + address private _issuer; + + constructor() EquippableToken() { + _setIssuer(_msgSender()); + } + + modifier onlyIssuer() { + require(_msgSender() == _issuer, "RMRK: Only issuer"); + _; + } + + function setIssuer(address issuer) external onlyIssuer { + _setIssuer(issuer); + } + + function _setIssuer(address issuer) private { + _issuer = issuer; + } + + function getIssuer() external view returns (address) { + return _issuer; + } + + function mint(address to, uint256 tokenId) external onlyIssuer { + _mint(to, tokenId); + } + + function nestMint( + address to, + uint256 tokenId, + uint256 destinationId + ) external { + _nestMint(to, tokenId, destinationId, ""); + } + + // Utility transfers: + + function transfer(address to, uint256 tokenId) public virtual { + transferFrom(_msgSender(), to, tokenId); + } + + function nestTransfer( + address to, + uint256 tokenId, + uint256 destinationId + ) public virtual { + nestTransferFrom(_msgSender(), to, tokenId, destinationId, ""); + } + + function addAssetToToken( + uint256 tokenId, + uint64 assetId, + uint64 replacesAssetWithId + ) external onlyIssuer { + _addAssetToToken(tokenId, assetId, replacesAssetWithId); + } + + function addAssetEntry( + uint64 id, + string memory metadataURI + ) external onlyIssuer { + _addAssetEntry(id, metadataURI); + } + + function addEquippableAssetEntry( + uint64 id, + uint64 equippableGroupId, + address catalogAddress, + string memory metadataURI, + uint64[] calldata partIds + ) external onlyIssuer { + _addAssetEntry( + id, + equippableGroupId, + catalogAddress, + metadataURI, + partIds + ); + } + + function setValidParentForEquippableGroup( + uint64 equippableGroupId, + address parentAddress, + uint64 partId + ) external { + _setValidParentForEquippableGroup( + equippableGroupId, + parentAddress, + partId + ); + } +} diff --git a/assets/eip-6220/contracts/mocks/NonReceiverMock.sol b/assets/eip-6220/contracts/mocks/NonReceiverMock.sol new file mode 100644 index 00000000000000..e9d2d6ed3ecb2e --- /dev/null +++ b/assets/eip-6220/contracts/mocks/NonReceiverMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.15; + +contract NonReceiverMock { + function dummy() external {} +} diff --git a/assets/eip-6220/contracts/security/ReentrancyGuard.sol b/assets/eip-6220/contracts/security/ReentrancyGuard.sol new file mode 100644 index 00000000000000..5fab06e46f0c5c --- /dev/null +++ b/assets/eip-6220/contracts/security/ReentrancyGuard.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) + +pragma solidity ^0.8.16; + +error RentrantCall(); + +/** + * @title ReentrancyGuard + * @notice Smart contract used to guard against potential reentrancy exploits. + * @dev Contract module that helps prevent reentrant calls to a function. + * + * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier + * available, which can be applied to functions to make sure there are no nested + * (reentrant) calls to them. + * + * Note that because there is a single `nonReentrant` guard, functions marked as + * `nonReentrant` may not call one another. This can be worked around by making + * those functions `private`, and then adding `external` `nonReentrant` entry + * points to them. + * + * TIP: If you would like to learn more about reentrancy and alternative ways + * to protect against it, check out our blog post + * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. + */ +abstract contract ReentrancyGuard { + // Booleans are more expensive than uint256 or any type that takes up a full + // word because each write operation emits an extra SLOAD to first read the + // slot's contents, replace the bits taken up by the boolean, and then write + // back. This is the compiler's defense against contract upgrades and + // pointer aliasing, and it cannot be disabled. + + // The values being non-zero value makes deployment a bit more expensive, + // but in exchange the refund on every call to nonReentrant will be lower in + // amount. Since refunds are capped to a percentage of the total + // transaction's gas, it is best to keep them low in cases like this one, to + // increase the likelihood of the full refund coming into effect. + uint256 private constant _NOT_ENTERED = 1; + uint256 private constant _ENTERED = 2; + + uint256 private _status; + + /** + * @notice Initializes the ReentrancyGuard with the `_status` of `_NOT_ENTERED`. + */ + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @notice Used to ensure that the function it is applied to cannot be reentered. + * @dev Prevents a contract from calling itself, directly or indirectly. + * Calling a `nonReentrant` function from another `nonReentrant` + * function is not supported. It is possible to prevent this from happening + * by making the `nonReentrant` function external, and making it call a + * `private` function that does the actual work. + */ + modifier nonReentrant() { + _nonReentrantIn(); + _; + // By storing the original value once again, a refund is triggered (see + // https://eips.ethereum.org/EIPS/eip-2200) + _status = _NOT_ENTERED; + } + + /** + * @notice Used to ensure that the current call is not a reentrant call. + * @dev If reentrant call is detected, the execution will be reverted. + */ + function _nonReentrantIn() private { + // On the first call to nonReentrant, _notEntered will be true + if (_status == _ENTERED) revert RentrantCall(); + + // Any calls to nonReentrant after this point will fail + _status = _ENTERED; + } +} diff --git a/assets/eip-6220/contracts/utils/EquipRenderUtils.sol b/assets/eip-6220/contracts/utils/EquipRenderUtils.sol new file mode 100644 index 00000000000000..6637d5a86d8829 --- /dev/null +++ b/assets/eip-6220/contracts/utils/EquipRenderUtils.sol @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "../ICatalog.sol"; +import "../IEquippable.sol"; +import "../library/EquippableLib.sol"; + +error TokenHasNoAssets(); +error NotComposableAsset(); + +/** + * @title EquipRenderUtils + * @author RMRK team + * @notice Smart contract of the RMRK Equip render utils module. + * @dev Extra utility functions for composing RMRK extended assets. + */ +contract EquipRenderUtils { + using EquippableLib for uint64[]; + + /** + * @notice The structure used to display a full information of an active asset. + * @return id ID of the asset + * @return equppableGroupId ID of the equippable group this asset belongs to + * @return priority Priority of the asset in the active assets array it belongs to + * @return catalogAddress Address of the `Catalog` smart contract this asset belongs to + * @return metadata Metadata URI of the asset + * @return partIds[] An array of IDs of fixed and slot parts present in the asset + */ + struct ExtendedActiveAsset { + uint64 id; + uint64 equippableGroupId; + uint16 priority; + address catalogAddress; + string metadata; + uint64[] partIds; + } + + /** + * @notice The structure used to display a full information of a pending asset. + * @return id ID of the asset + * @return equppableGroupId ID of the equippable group this asset belongs to + * @return acceptRejectIndex The index of the given asset in the pending assets array it belongs to + * @return replacesAssetWithId ID of the asset the given asset will replace if accepted + * @return catalogAddress Address of the `Catalog` smart contract this asset belongs to + * @return metadata Metadata URI of the asset + * @return partIds[] An array of IDs of fixed and slot parts present in the asset + */ + struct ExtendedPendingAsset { + uint64 id; + uint64 equippableGroupId; + uint128 acceptRejectIndex; + uint64 replacesAssetWithId; + address catalogAddress; + string metadata; + uint64[] partIds; + } + + /** + * @notice The structure used to display a full information of an equippend slot part. + * @return partId ID of the slot part + * @return childAssetId ID of the child asset equipped into the slot part + * @return z The z value of the part defining how it should be rendered when presenting the full NFT + * @return childAddress Address of the collection smart contract of the child token equipped into the slot + * @return childId ID of the child token equipped into the slot + * @return childAssetMetadata Metadata URI of the child token equipped into the slot + * @return partMetadata Metadata URI of the given slot part + */ + struct EquippedSlotPart { + uint64 partId; + uint64 childAssetId; + uint8 z; //1 byte + address childAddress; + uint256 childId; + string childAssetMetadata; //n bytes 32+ + string partMetadata; //n bytes 32+ + } + + /** + * @notice Used to provide data about fixed parts. + * @return partId ID of the part + * @return z The z value of the asset, specifying how the part should be rendered in a composed NFT + * @return matadataURI The metadata URI of the fixed part + */ + struct FixedPart { + uint64 partId; + uint8 z; //1 byte + string metadataURI; //n bytes 32+ + } + + /** + * @notice Used to get extended active assets of the given token. + * @dev The full `ExtendedActiveAsset` looks like this: + * [ + * ID, + * equippableGroupId, + * priority, + * catalogAddress, + * metadata, + * [ + * fixedPartId0, + * fixedPartId1, + * fixedPartId2, + * slotPartId0, + * slotPartId1, + * slotPartId2 + * ] + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the extended active assets for + * @return sturct[] An array of ExtendedActiveAssets present on the given token + */ + function getExtendedActiveAssets(address target, uint256 tokenId) + public + view + virtual + returns (ExtendedActiveAsset[] memory) + { + IEquippable target_ = IEquippable(target); + + uint64[] memory assets = target_.getActiveAssets(tokenId); + uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); + uint256 len = assets.length; + if (len == 0) { + revert TokenHasNoAssets(); + } + + ExtendedActiveAsset[] memory activeAssets = new ExtendedActiveAsset[]( + len + ); + + for (uint256 i; i < len; ) { + ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] memory partIds + ) = target_.getAssetAndEquippableData(tokenId, assets[i]); + activeAssets[i] = ExtendedActiveAsset({ + id: assets[i], + equippableGroupId: equippableGroupId, + priority: priorities[i], + catalogAddress: catalogAddress, + metadata: metadataURI, + partIds: partIds + }); + unchecked { + ++i; + } + } + return activeAssets; + } + + /** + * @notice Used to get the extended pending assets of the given token. + * @dev The full `ExtendedPendingAsset` looks like this: + * [ + * ID, + * equippableGroupId, + * acceptRejectIndex, + * replacesAssetWithId, + * catalogAddress, + * metadata, + * [ + * fixedPartId0, + * fixedPartId1, + * fixedPartId2, + * slotPartId0, + * slotPartId1, + * slotPartId2 + * ] + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the extended pending assets for + * @return sturct[] An array of ExtendedPendingAssets present on the given token + */ + function getExtendedPendingAssets(address target, uint256 tokenId) + public + view + virtual + returns (ExtendedPendingAsset[] memory) + { + IEquippable target_ = IEquippable(target); + + uint64[] memory assets = target_.getPendingAssets(tokenId); + uint256 len = assets.length; + if (len == 0) { + revert TokenHasNoAssets(); + } + + ExtendedPendingAsset[] + memory pendingAssets = new ExtendedPendingAsset[](len); + uint64 replacesAssetWithId; + for (uint256 i; i < len; ) { + ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + uint64[] memory partIds + ) = target_.getAssetAndEquippableData(tokenId, assets[i]); + replacesAssetWithId = target_.getAssetReplacements( + tokenId, + assets[i] + ); + pendingAssets[i] = ExtendedPendingAsset({ + id: assets[i], + equippableGroupId: equippableGroupId, + acceptRejectIndex: uint128(i), + replacesAssetWithId: replacesAssetWithId, + catalogAddress: catalogAddress, + metadata: metadataURI, + partIds: partIds + }); + unchecked { + ++i; + } + } + return pendingAssets; + } + + /** + * @notice Used to retrieve the equipped parts of the given token. + * @dev NOTE: Some of the equipped children might be empty. + * @dev The full `Equipment` struct looks like this: + * [ + * assetId, + * childAssetId, + * childId, + * childEquippableAddress + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the equipped items in the asset for + * @param assetId ID of the asset being queried for equipped parts + * @return slotPartIds An array of the IDs of the slot parts present in the given asset + * @return childrenEquipped An array of `Equipment` structs containing info about the equipped children + */ + function getEquipped( + address target, + uint64 tokenId, + uint64 assetId + ) + public + view + returns ( + uint64[] memory slotPartIds, + IEquippable.Equipment[] memory childrenEquipped + ) + { + IEquippable target_ = IEquippable(target); + + (, , address catalogAddress, uint64[] memory partIds) = target_ + .getAssetAndEquippableData(tokenId, assetId); + + (slotPartIds, ) = splitSlotAndFixedParts(partIds, catalogAddress); + childrenEquipped = new IEquippable.Equipment[](slotPartIds.length); + + uint256 len = slotPartIds.length; + for (uint256 i; i < len; ) { + IEquippable.Equipment memory equipment = target_.getEquipment( + tokenId, + catalogAddress, + slotPartIds[i] + ); + if (equipment.assetId == assetId) { + childrenEquipped[i] = equipment; + } + unchecked { + ++i; + } + } + } + + /** + * @notice Used to compose the given equippables. + * @dev The full `FixedPart` struct looks like this: + * [ + * partId, + * z, + * metadataURI + * ] + * @dev The full `EquippedSlotPart` struct looks like this: + * [ + * partId, + * childAssetId, + * z, + * childAddress, + * childId, + * childAssetMetadata, + * partMetadata + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to compose the equipped items in the asset for + * @param assetId ID of the asset being queried for equipped parts + * @return metadataURI Metadata URI of the asset + * @return equippableGroupId Equippable group ID of the asset + * @return catalogAddress Address of the catalog to which the asset belongs to + * @return fixedParts An array of fixed parts respresented by the `FixedPart` structs present on the asset + * @return slotParts An array of slot parts represented by the `EquippedSlotPart` structs present on the asset + */ + function composeEquippables( + address target, + uint256 tokenId, + uint64 assetId + ) + public + view + returns ( + string memory metadataURI, + uint64 equippableGroupId, + address catalogAddress, + FixedPart[] memory fixedParts, + EquippedSlotPart[] memory slotParts + ) + { + IEquippable target_ = IEquippable(target); + uint64[] memory partIds; + + // If token does not have uint64[] memory slotPartId to save the asset, it would fail here. + (metadataURI, equippableGroupId, catalogAddress, partIds) = target_ + .getAssetAndEquippableData(tokenId, assetId); + if (catalogAddress == address(0)) revert NotComposableAsset(); + + ( + uint64[] memory slotPartIds, + uint64[] memory fixedPartIds + ) = splitSlotAndFixedParts(partIds, catalogAddress); + + // Fixed parts: + fixedParts = new FixedPart[](fixedPartIds.length); + + uint256 len = fixedPartIds.length; + if (len != 0) { + ICatalog.Part[] memory catalogFixedParts = ICatalog( + catalogAddress + ).getParts(fixedPartIds); + for (uint256 i; i < len; ) { + fixedParts[i] = FixedPart({ + partId: fixedPartIds[i], + z: catalogFixedParts[i].z, + metadataURI: catalogFixedParts[i].metadataURI + }); + unchecked { + ++i; + } + } + } + + slotParts = getEquippedSlotParts( + target_, + tokenId, + assetId, + catalogAddress, + slotPartIds + ); + } + + /** + * @notice Used to retrieve the equipped slot parts. + * @dev The full `EquippedSlotPart` struct looks like this: + * [ + * partId, + * childAssetId, + * z, + * childAddress, + * childId, + * childAssetMetadata, + * partMetadata + * ] + * @param target_ An address of the `IEquippable` smart contract to retrieve the equipped slot parts from. + * @param tokenId ID of the token for which to retrieve the equipped slot parts + * @param assetId ID of the asset on the token to retrieve the equipped slot parts + * @param catalogAddress The address of the catalog to which the given asset belongs to + * @param slotPartIds An array of slot part IDs in the asset for which to retrieve the equipped slot parts + * @return slotParts An array of `EquippedSlotPart` structs representing the equipped slot parts + */ + function getEquippedSlotParts( + IEquippable target_, + uint256 tokenId, + uint64 assetId, + address catalogAddress, + uint64[] memory slotPartIds + ) private view returns (EquippedSlotPart[] memory slotParts) { + slotParts = new EquippedSlotPart[](slotPartIds.length); + uint256 len = slotPartIds.length; + + if (len != 0) { + string memory metadata; + ICatalog.Part[] memory catalogSlotParts = ICatalog(catalogAddress) + .getParts(slotPartIds); + for (uint256 i; i < len; ) { + IEquippable.Equipment memory equipment = target_.getEquipment( + tokenId, + catalogAddress, + slotPartIds[i] + ); + if (equipment.assetId == assetId) { + metadata = IEquippable(equipment.childEquippableAddress) + .getAssetMetadata( + equipment.childId, + equipment.childAssetId + ); + slotParts[i] = EquippedSlotPart({ + partId: slotPartIds[i], + childAssetId: equipment.childAssetId, + z: catalogSlotParts[i].z, + childId: equipment.childId, + childAddress: equipment.childEquippableAddress, + childAssetMetadata: metadata, + partMetadata: catalogSlotParts[i].metadataURI + }); + } else { + slotParts[i] = EquippedSlotPart({ + partId: slotPartIds[i], + childAssetId: uint64(0), + z: catalogSlotParts[i].z, + childId: uint256(0), + childAddress: address(0), + childAssetMetadata: "", + partMetadata: catalogSlotParts[i].metadataURI + }); + } + unchecked { + ++i; + } + } + } + } + + /** + * @notice Used to split slot and fixed parts. + * @param allPartIds[] An array of `Part` IDs containing both, `Slot` and `Fixed` parts + * @param catalogAddress An address of the catalog to which the given `Part`s belong to + * @return slotPartIds An array of IDs of the `Slot` parts included in the `allPartIds` + * @return fixedPartIds An array of IDs of the `Fixed` parts included in the `allPartIds` + */ + function splitSlotAndFixedParts( + uint64[] memory allPartIds, + address catalogAddress + ) + public + view + returns (uint64[] memory slotPartIds, uint64[] memory fixedPartIds) + { + ICatalog.Part[] memory allParts = ICatalog(catalogAddress) + .getParts(allPartIds); + uint256 numFixedParts; + uint256 numSlotParts; + + uint256 numParts = allPartIds.length; + // This for loop is just to discover the right size of the split arrays, since we can't create them dynamically + for (uint256 i; i < numParts; ) { + if (allParts[i].itemType == ICatalog.ItemType.Fixed) + numFixedParts += 1; + // We could just take the numParts - numFixedParts, but it doesn't hurt to double check it's not an uninitialized part: + else if (allParts[i].itemType == ICatalog.ItemType.Slot) + numSlotParts += 1; + unchecked { + ++i; + } + } + + slotPartIds = new uint64[](numSlotParts); + fixedPartIds = new uint64[](numFixedParts); + uint256 slotPartsIndex; + uint256 fixedPartsIndex; + + // This for loop is to actually fill the split arrays + for (uint256 i; i < numParts; ) { + if (allParts[i].itemType == ICatalog.ItemType.Fixed) { + fixedPartIds[fixedPartsIndex] = allPartIds[i]; + fixedPartsIndex += 1; + } else if (allParts[i].itemType == ICatalog.ItemType.Slot) { + slotPartIds[slotPartsIndex] = allPartIds[i]; + slotPartsIndex += 1; + } + unchecked { + ++i; + } + } + } +} diff --git a/assets/eip-6220/contracts/utils/MultiAssetRenderUtils.sol b/assets/eip-6220/contracts/utils/MultiAssetRenderUtils.sol new file mode 100644 index 00000000000000..47d8dcbe945ee0 --- /dev/null +++ b/assets/eip-6220/contracts/utils/MultiAssetRenderUtils.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity ^0.8.16; + +import "../IERC5773.sol"; + +error TokenHasNoAssets(); + +/** + * @title MultiAssetRenderUtils + * @author RMRK team + */ +contract MultiAssetRenderUtils { + uint16 private constant _LOWEST_POSSIBLE_PRIORITY = 2**16 - 1; + + /** + * @notice The structure used to display information about an active asset. + * @return id ID of the asset + * @return priority The priority assigned to the asset + * @return metadata The metadata URI of the asset + */ + struct ActiveAsset { + uint64 id; + uint16 priority; + string metadata; + } + + /** + * @notice The structure used to display information about a pending asset. + * @return id ID of the asset + * @return acceptRejectIndex An index to use in order to accept or reject the given asset + * @return replacesAssetWithId ID of the asset that would be replaced if this asset gets accepted + * @return metadata The metadata URI of the asset + */ + struct PendingAsset { + uint64 id; + uint128 acceptRejectIndex; + uint64 replacesAssetWithId; + string metadata; + } + + /** + * @notice Used to get the active assets of the given token. + * @dev The full `ActiveAsset` looks like this: + * [ + * id, + * priority, + * metadata + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the active assets for + * @return struct[] An array of ActiveAssets present on the given token + */ + function getActiveAssets(address target, uint256 tokenId) + public + view + virtual + returns (ActiveAsset[] memory) + { + IERC5773 target_ = IERC5773(target); + + uint64[] memory assets = target_.getActiveAssets(tokenId); + uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); + uint256 len = assets.length; + if (len == 0) { + revert TokenHasNoAssets(); + } + + ActiveAsset[] memory activeAssets = new ActiveAsset[](len); + string memory metadata; + for (uint256 i; i < len; ) { + metadata = target_.getAssetMetadata(tokenId, assets[i]); + activeAssets[i] = ActiveAsset({ + id: assets[i], + priority: priorities[i], + metadata: metadata + }); + unchecked { + ++i; + } + } + return activeAssets; + } + + /** + * @notice Used to get the pending assets of the given token. + * @dev The full `PendingAsset` looks like this: + * [ + * id, + * acceptRejectIndex, + * replacesAssetWithId, + * metadata + * ] + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the pending assets for + * @return struct[] An array of PendingAssets present on the given token + */ + function getPendingAssets(address target, uint256 tokenId) + public + view + virtual + returns (PendingAsset[] memory) + { + IERC5773 target_ = IERC5773(target); + + uint64[] memory assets = target_.getPendingAssets(tokenId); + uint256 len = assets.length; + if (len == 0) { + revert TokenHasNoAssets(); + } + + PendingAsset[] memory pendingAssets = new PendingAsset[](len); + string memory metadata; + uint64 replacesAssetWithId; + for (uint256 i; i < len; ) { + metadata = target_.getAssetMetadata(tokenId, assets[i]); + replacesAssetWithId = target_.getAssetReplacements( + tokenId, + assets[i] + ); + pendingAssets[i] = PendingAsset({ + id: assets[i], + acceptRejectIndex: uint128(i), + replacesAssetWithId: replacesAssetWithId, + metadata: metadata + }); + unchecked { + ++i; + } + } + return pendingAssets; + } + + /** + * @notice Used to retrieve the metadata URI of specified assets in the specified token. + * @dev Requirements: + * + * - `assetIds` must exist. + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token to retrieve the specified assets for + * @param assetIds[] An array of asset IDs for which to retrieve the metadata URIs + * @return string[] An array of metadata URIs belonging to specified assets + */ + function getAssetsById( + address target, + uint256 tokenId, + uint64[] calldata assetIds + ) public view virtual returns (string[] memory) { + IERC5773 target_ = IERC5773(target); + uint256 len = assetIds.length; + string[] memory assets = new string[](len); + for (uint256 i; i < len; ) { + assets[i] = target_.getAssetMetadata(tokenId, assetIds[i]); + unchecked { + ++i; + } + } + return assets; + } + + /** + * @notice Used to retrieve the metadata URI of the specified token's asset with the highest priority. + * @param target Address of the smart contract of the given token + * @param tokenId ID of the token for which to retrieve the metadata URI of the asset with the highest priority + * @return string The metadata URI of the asset with the highest priority + */ + function getTopAssetMetaForToken(address target, uint256 tokenId) + external + view + returns (string memory) + { + IERC5773 target_ = IERC5773(target); + uint16[] memory priorities = target_.getActiveAssetPriorities(tokenId); + uint64[] memory assets = target_.getActiveAssets(tokenId); + uint256 len = priorities.length; + if (len == 0) { + revert TokenHasNoAssets(); + } + + uint16 maxPriority = _LOWEST_POSSIBLE_PRIORITY; + uint64 maxPriorityAsset; + for (uint64 i; i < len; ) { + uint16 currentPrio = priorities[i]; + if (currentPrio < maxPriority) { + maxPriority = currentPrio; + maxPriorityAsset = assets[i]; + } + unchecked { + ++i; + } + } + return target_.getAssetMetadata(tokenId, maxPriorityAsset); + } +} diff --git a/assets/eip-6220/hardhat.config.ts b/assets/eip-6220/hardhat.config.ts new file mode 100644 index 00000000000000..68bbc2796a8937 --- /dev/null +++ b/assets/eip-6220/hardhat.config.ts @@ -0,0 +1,18 @@ +import { HardhatUserConfig } from 'hardhat/config'; +import '@nomicfoundation/hardhat-chai-matchers'; +import '@nomiclabs/hardhat-etherscan'; +import '@typechain/hardhat'; + +const config: HardhatUserConfig = { + solidity: { + version: '0.8.16', + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, +}; + +export default config; diff --git a/assets/eip-6220/package.json b/assets/eip-6220/package.json new file mode 100644 index 00000000000000..7f79171d1eff2c --- /dev/null +++ b/assets/eip-6220/package.json @@ -0,0 +1,29 @@ +{ + "name": "equippable-tokens", + "dependencies": { + "@openzeppelin/contracts": "^4.6.0" + }, + "devDependencies": { + "@nomicfoundation/hardhat-chai-matchers": "^1.0.1", + "@nomicfoundation/hardhat-network-helpers": "^1.0.3", + "@nomiclabs/hardhat-ethers": "^2.2.1", + "@nomiclabs/hardhat-etherscan": "^3.1.0", + "@openzeppelin/test-helpers": "^0.5.15", + "@primitivefi/hardhat-dodoc": "^0.2.3", + "@typechain/ethers-v5": "^10.1.0", + "@typechain/hardhat": "^6.1.2", + "@types/chai": "^4.3.1", + "@types/mocha": "^9.1.0", + "@types/node": "^18.0.3", + "@typescript-eslint/eslint-plugin": "^5.30.6", + "@typescript-eslint/parser": "^5.30.6", + "chai": "^4.3.6", + "ethers": "^5.6.9", + "hardhat": "^2.12.2", + "solc": "^0.8.9", + "ts-node": "^10.8.2", + "typechain": "^8.1.0", + "typescript": "^4.7.4", + "walk-sync": "^3.0.0" + } +} diff --git a/assets/eip-6220/test/catalog.ts b/assets/eip-6220/test/catalog.ts new file mode 100644 index 00000000000000..8c0e34ea893159 --- /dev/null +++ b/assets/eip-6220/test/catalog.ts @@ -0,0 +1,280 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { CatalogMock } from '../typechain-types'; + +async function catalogFixture(): Promise { + const Catalog = await ethers.getContractFactory('CatalogMock'); + const testCatalog = await Catalog.deploy('ipfs//:meta', 'misc'); + await testCatalog.deployed(); + + return testCatalog; +} + +describe('CatalogMock', async () => { + let testCatalog: CatalogMock; + + let addrs: SignerWithAddress[]; + const metadataUriDefault = 'src'; + + const noType = 0; + const slotType = 1; + const fixedType = 2; + + const sampleSlotPartData = { + itemType: slotType, + z: 0, + equippable: [], + metadataURI: metadataUriDefault, + }; + + beforeEach(async () => { + [, ...addrs] = await ethers.getSigners(); + testCatalog = await loadFixture(catalogFixture); + }); + + describe('Init Catalog', async function () { + it('has right metadataURI', async function () { + expect(await testCatalog.getMetadataURI()).to.equal('ipfs//:meta'); + }); + + it('has right type', async function () { + expect(await testCatalog.getType()).to.equal('misc'); + }); + + it('supports interface', async function () { + expect(await testCatalog.supportsInterface('0xd912401f')).to.equal(true); + }); + + it('does not support other interfaces', async function () { + expect(await testCatalog.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('add catalog entries', async function () { + it('can add fixed part', async function () { + const partId = 1; + const partData = { + itemType: fixedType, + z: 0, + equippable: [], + metadataURI: metadataUriDefault, + }; + + await testCatalog.addPart({ partId: partId, part: partData }); + expect(await testCatalog.getPart(partId)).to.eql([2, 0, [], metadataUriDefault]); + }); + + it('can add slot part', async function () { + const partId = 2; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + expect(await testCatalog.getPart(partId)).to.eql([1, 0, [], metadataUriDefault]); + }); + + it('can add parts list', async function () { + const partId = 1; + const partId2 = 2; + const partData1 = { + itemType: slotType, + z: 0, + equippable: [], + metadataURI: 'src1', + }; + const partData2 = { + itemType: fixedType, + z: 1, + equippable: [], + metadataURI: 'src2', + }; + await testCatalog.addPartList([ + { partId: partId, part: partData1 }, + { partId: partId2, part: partData2 }, + ]); + expect(await testCatalog.getParts([partId, partId2])).to.eql([ + [slotType, 0, [], 'src1'], + [fixedType, 1, [], 'src2'], + ]); + }); + + it('cannot add part with id 0', async function () { + const partId = 0; + await expect( + testCatalog.addPart({ partId: partId, part: sampleSlotPartData }), + ).to.be.revertedWithCustomError(testCatalog, 'IdZeroForbidden'); + }); + + it('cannot add part with existing partId', async function () { + const partId = 3; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await expect( + testCatalog.addPart({ partId: partId, part: sampleSlotPartData }), + ).to.be.revertedWithCustomError(testCatalog, 'PartAlreadyExists'); + }); + + it('cannot add part with item type None', async function () { + const partId = 1; + const badPartData = { + itemType: noType, + z: 0, + equippable: [], + metadataURI: metadataUriDefault, + }; + await expect( + testCatalog.addPart({ partId: partId, part: badPartData }), + ).to.be.revertedWithCustomError(testCatalog, 'BadConfig'); + }); + + it('cannot add fixed part with equippable addresses', async function () { + const partId = 1; + const badPartData = { + itemType: fixedType, + z: 0, + equippable: [addrs[3].address], + metadataURI: metadataUriDefault, + }; + await expect( + testCatalog.addPart({ partId: partId, part: badPartData }), + ).to.be.revertedWithCustomError(testCatalog, 'BadConfig'); + }); + + it('is not equippable if address was not added', async function () { + const partId = 4; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + expect(await testCatalog.checkIsEquippable(partId, addrs[1].address)).to.eql(false); + }); + + it('is equippable if added in the part definition', async function () { + const partId = 1; + const partData = { + itemType: slotType, + z: 0, + equippable: [addrs[1].address, addrs[2].address], + metadataURI: metadataUriDefault, + }; + await testCatalog.addPart({ partId: partId, part: partData }); + expect(await testCatalog.checkIsEquippable(partId, addrs[2].address)).to.eql(true); + }); + + it('is equippable if added afterward', async function () { + const partId = 1; + await expect(testCatalog.addPart({ partId: partId, part: sampleSlotPartData })) + .to.emit(testCatalog, 'AddedPart') + .withArgs( + partId, + sampleSlotPartData.itemType, + sampleSlotPartData.z, + sampleSlotPartData.equippable, + sampleSlotPartData.metadataURI, + ); + await expect(testCatalog.addEquippableAddresses(partId, [addrs[1].address])) + .to.emit(testCatalog, 'AddedEquippables') + .withArgs(partId, [addrs[1].address]); + expect(await testCatalog.checkIsEquippable(partId, addrs[1].address)).to.eql(true); + }); + + it('is equippable if set afterward', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await expect(testCatalog.setEquippableAddresses(partId, [addrs[1].address])) + .to.emit(testCatalog, 'SetEquippables') + .withArgs(partId, [addrs[1].address]); + expect(await testCatalog.checkIsEquippable(partId, addrs[1].address)).to.eql(true); + }); + + it('is equippable if set to all', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await expect(testCatalog.setEquippableToAll(partId)) + .to.emit(testCatalog, 'SetEquippableToAll') + .withArgs(partId); + expect(await testCatalog.checkIsEquippableToAll(partId)).to.eql(true); + expect(await testCatalog.checkIsEquippable(partId, addrs[1].address)).to.eql(true); + }); + + it('cannot add nor set equippable addresses for non existing part', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await expect(testCatalog.addEquippableAddresses(partId, [])).to.be.revertedWithCustomError( + testCatalog, + 'ZeroLengthIdsPassed', + ); + await expect(testCatalog.setEquippableAddresses(partId, [])).to.be.revertedWithCustomError( + testCatalog, + 'ZeroLengthIdsPassed', + ); + }); + + it('cannot add nor set empty list of equippable addresses', async function () { + const NonExistingPartId = 1; + await expect( + testCatalog.addEquippableAddresses(NonExistingPartId, [addrs[1].address]), + ).to.be.revertedWithCustomError(testCatalog, 'PartDoesNotExist'); + await expect( + testCatalog.setEquippableAddresses(NonExistingPartId, [addrs[1].address]), + ).to.be.revertedWithCustomError(testCatalog, 'PartDoesNotExist'); + await expect(testCatalog.setEquippableToAll(NonExistingPartId)).to.be.revertedWithCustomError( + testCatalog, + 'PartDoesNotExist', + ); + }); + + it('cannot add nor set equippable addresses to non slot part', async function () { + const fixedPartId = 1; + const partData = { + itemType: fixedType, // This is what we're testing + z: 0, + equippable: [], + metadataURI: metadataUriDefault, + }; + await testCatalog.addPart({ partId: fixedPartId, part: partData }); + await expect( + testCatalog.addEquippableAddresses(fixedPartId, [addrs[1].address]), + ).to.be.revertedWithCustomError(testCatalog, 'PartIsNotSlot'); + await expect( + testCatalog.setEquippableAddresses(fixedPartId, [addrs[1].address]), + ).to.be.revertedWithCustomError(testCatalog, 'PartIsNotSlot'); + await expect(testCatalog.setEquippableToAll(fixedPartId)).to.be.revertedWithCustomError( + testCatalog, + 'PartIsNotSlot', + ); + }); + + it('cannot set equippable to all on non existing part', async function () { + const nonExistingPartId = 1; + await expect(testCatalog.setEquippableToAll(nonExistingPartId)).to.be.revertedWithCustomError( + testCatalog, + 'PartDoesNotExist', + ); + }); + + it('resets equippable to all if addresses are set', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await testCatalog.setEquippableToAll(partId); + + // This should reset it: + testCatalog.setEquippableAddresses(partId, [addrs[1].address]); + expect(await testCatalog.checkIsEquippableToAll(partId)).to.eql(false); + }); + + it('resets equippable to all if addresses are added', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await testCatalog.setEquippableToAll(partId); + + // This should reset it: + await testCatalog.addEquippableAddresses(partId, [addrs[1].address]); + expect(await testCatalog.checkIsEquippableToAll(partId)).to.eql(false); + }); + + it('can reset equippable addresses', async function () { + const partId = 1; + await testCatalog.addPart({ partId: partId, part: sampleSlotPartData }); + await testCatalog.addEquippableAddresses(partId, [addrs[1].address, addrs[2].address]); + + await testCatalog.resetEquippableAddresses(partId); + expect(await testCatalog.checkIsEquippable(partId, addrs[1].address)).to.eql(false); + }); + }); +}); diff --git a/assets/eip-6220/test/equippableFixedParts.ts b/assets/eip-6220/test/equippableFixedParts.ts new file mode 100644 index 00000000000000..9a54684b5e48c8 --- /dev/null +++ b/assets/eip-6220/test/equippableFixedParts.ts @@ -0,0 +1,588 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber } from 'ethers'; +import { ethers } from 'hardhat'; +import { CatalogMock, EquippableTokenMock, EquipRenderUtils } from '../typechain-types'; + +let addrs: SignerWithAddress[]; + +const partIdForHead1 = 1; +const partIdForHead2 = 2; +const partIdForHead3 = 3; +const partIdForBody1 = 4; +const partIdForBody2 = 5; +const partIdForHair1 = 6; +const partIdForHair2 = 7; +const partIdForHair3 = 8; +const partIdForMaskCatalog1 = 9; +const partIdForMaskCatalog2 = 10; +const partIdForMaskCatalog3 = 11; +const partIdForEars1 = 12; +const partIdForEars2 = 13; +const partIdForHorns1 = 14; +const partIdForHorns2 = 15; +const partIdForHorns3 = 16; +const partIdForMaskCatalogEquipped1 = 17; +const partIdForMaskCatalogEquipped2 = 18; +const partIdForMaskCatalogEquipped3 = 19; +const partIdForEarsEquipped1 = 20; +const partIdForEarsEquipped2 = 21; +const partIdForHornsEquipped1 = 22; +const partIdForHornsEquipped2 = 23; +const partIdForHornsEquipped3 = 24; +const partIdForMask = 25; + +const uniqueNeons = 10; +const uniqueMasks = 4; +// Ids could be the same since they are different collections, but to avoid log problems we have them unique +const neons: number[] = []; +const masks: number[] = []; + +const neonResIds = [100, 101, 102, 103, 104]; +const maskAssetsFull = [1, 2, 3, 4]; // Must match the total of uniqueAssets +const maskAssetsEquip = [5, 6, 7, 8]; // Must match the total of uniqueAssets +const maskpableGroupId = 1; // Assets to equip will all use this + +enum ItemType { + None, + Slot, + Fixed, +} + +let nextTokenId = 1; +let nextChildTokenId = 100; + +async function mint(token: EquippableTokenMock, to: string): Promise { + const tokenId = nextTokenId; + nextTokenId++; + await token['mint(address,uint256)'](to, tokenId); + return tokenId; +} + +async function nestMint(token: EquippableTokenMock, to: string, parentId: number): Promise { + const childTokenId = nextChildTokenId; + nextChildTokenId++; + await token['nestMint(address,uint256,uint256)'](to, childTokenId, parentId); + return childTokenId; +} + +async function setupContextForParts( + catalog: CatalogMock, + neon: EquippableTokenMock, + mask: EquippableTokenMock, +) { + [, ...addrs] = await ethers.getSigners(); + await setupCatalog(); + + await mintNeons(); + await mintMasks(); + + await addAssetsToNeon(); + await addAssetsToMask(); + + async function setupCatalog(): Promise { + const partForHead1 = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'ipfs://head1.png', + }; + const partForHead2 = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'ipfs://head2.png', + }; + const partForHead3 = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'ipfs://head3.png', + }; + const partForBody1 = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'ipfs://body1.png', + }; + const partForBody2 = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'ipfs://body2.png', + }; + const partForHair1 = { + itemType: ItemType.Fixed, + z: 2, + equippable: [], + metadataURI: 'ipfs://hair1.png', + }; + const partForHair2 = { + itemType: ItemType.Fixed, + z: 2, + equippable: [], + metadataURI: 'ipfs://hair2.png', + }; + const partForHair3 = { + itemType: ItemType.Fixed, + z: 2, + equippable: [], + metadataURI: 'ipfs://hair3.png', + }; + const partForMaskCatalog1 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalog1.png', + }; + const partForMaskCatalog2 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalog2.png', + }; + const partForMaskCatalog3 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalog3.png', + }; + const partForEars1 = { + itemType: ItemType.Fixed, + z: 4, + equippable: [], + metadataURI: 'ipfs://ears1.png', + }; + const partForEars2 = { + itemType: ItemType.Fixed, + z: 4, + equippable: [], + metadataURI: 'ipfs://ears2.png', + }; + const partForHorns1 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://horn1.png', + }; + const partForHorns2 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://horn2.png', + }; + const partForHorns3 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://horn3.png', + }; + const partForMaskCatalogEquipped1 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalogEquipped1.png', + }; + const partForMaskCatalogEquipped2 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalogEquipped2.png', + }; + const partForMaskCatalogEquipped3 = { + itemType: ItemType.Fixed, + z: 3, + equippable: [], + metadataURI: 'ipfs://maskCatalogEquipped3.png', + }; + const partForEarsEquipped1 = { + itemType: ItemType.Fixed, + z: 4, + equippable: [], + metadataURI: 'ipfs://earsEquipped1.png', + }; + const partForEarsEquipped2 = { + itemType: ItemType.Fixed, + z: 4, + equippable: [], + metadataURI: 'ipfs://earsEquipped2.png', + }; + const partForHornsEquipped1 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://hornEquipped1.png', + }; + const partForHornsEquipped2 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://hornEquipped2.png', + }; + const partForHornsEquipped3 = { + itemType: ItemType.Fixed, + z: 5, + equippable: [], + metadataURI: 'ipfs://hornEquipped3.png', + }; + const partForMask = { + itemType: ItemType.Slot, + z: 2, + equippable: [mask.address], + metadataURI: '', + }; + + await catalog.addPartList([ + { partId: partIdForHead1, part: partForHead1 }, + { partId: partIdForHead2, part: partForHead2 }, + { partId: partIdForHead3, part: partForHead3 }, + { partId: partIdForBody1, part: partForBody1 }, + { partId: partIdForBody2, part: partForBody2 }, + { partId: partIdForHair1, part: partForHair1 }, + { partId: partIdForHair2, part: partForHair2 }, + { partId: partIdForHair3, part: partForHair3 }, + { partId: partIdForMaskCatalog1, part: partForMaskCatalog1 }, + { partId: partIdForMaskCatalog2, part: partForMaskCatalog2 }, + { partId: partIdForMaskCatalog3, part: partForMaskCatalog3 }, + { partId: partIdForEars1, part: partForEars1 }, + { partId: partIdForEars2, part: partForEars2 }, + { partId: partIdForHorns1, part: partForHorns1 }, + { partId: partIdForHorns2, part: partForHorns2 }, + { partId: partIdForHorns3, part: partForHorns3 }, + { partId: partIdForMaskCatalogEquipped1, part: partForMaskCatalogEquipped1 }, + { partId: partIdForMaskCatalogEquipped2, part: partForMaskCatalogEquipped2 }, + { partId: partIdForMaskCatalogEquipped3, part: partForMaskCatalogEquipped3 }, + { partId: partIdForEarsEquipped1, part: partForEarsEquipped1 }, + { partId: partIdForEarsEquipped2, part: partForEarsEquipped2 }, + { partId: partIdForHornsEquipped1, part: partForHornsEquipped1 }, + { partId: partIdForHornsEquipped2, part: partForHornsEquipped2 }, + { partId: partIdForHornsEquipped3, part: partForHornsEquipped3 }, + { partId: partIdForMask, part: partForMask }, + ]); + } + + async function mintNeons(): Promise { + // This array is reused, so we "empty" it before + neons.length = 0; + // Using only first 3 addresses to mint + for (let i = 0; i < uniqueNeons; i++) { + const newId = await mint(neon, addrs[i % 3].address); + neons.push(newId); + } + } + + async function mintMasks(): Promise { + // This array is reused, so we "empty" it before + masks.length = 0; + // Mint one weapon to neon + for (let i = 0; i < uniqueNeons; i++) { + const newId = await nestMint(mask, neon.address, neons[i]); + masks.push(newId); + await neon.connect(addrs[i % 3]).acceptChild(neons[i], 0, mask.address, newId); + } + } + + async function addAssetsToNeon(): Promise { + await neon.addEquippableAssetEntry(neonResIds[0], 0, catalog.address, 'ipfs:neonRes/1', [ + partIdForHead1, + partIdForBody1, + partIdForHair1, + partIdForMask, + ]); + await neon.addEquippableAssetEntry(neonResIds[1], 0, catalog.address, 'ipfs:neonRes/2', [ + partIdForHead2, + partIdForBody2, + partIdForHair2, + partIdForMask, + ]); + await neon.addEquippableAssetEntry(neonResIds[2], 0, catalog.address, 'ipfs:neonRes/3', [ + partIdForHead3, + partIdForBody1, + partIdForHair3, + partIdForMask, + ]); + await neon.addEquippableAssetEntry(neonResIds[3], 0, catalog.address, 'ipfs:neonRes/4', [ + partIdForHead1, + partIdForBody2, + partIdForHair2, + partIdForMask, + ]); + await neon.addEquippableAssetEntry(neonResIds[4], 0, catalog.address, 'ipfs:neonRes/1', [ + partIdForHead2, + partIdForBody1, + partIdForHair1, + partIdForMask, + ]); + + for (let i = 0; i < uniqueNeons; i++) { + await neon.addAssetToToken(neons[i], neonResIds[i % neonResIds.length], 0); + await neon.connect(addrs[i % 3]).acceptAsset(neons[i], 0, neonResIds[i % neonResIds.length]); + } + } + + async function addAssetsToMask(): Promise { + // Assets for full view, composed with fixed parts + await mask.addEquippableAssetEntry( + maskAssetsFull[0], + 0, // Not meant to equip + catalog.address, // Not meant to equip, but catalog needed for parts + `ipfs:weapon/full/${maskAssetsFull[0]}`, + [partIdForMaskCatalog1, partIdForHorns1, partIdForEars1], + ); + await mask.addEquippableAssetEntry( + maskAssetsFull[1], + 0, // Not meant to equip + catalog.address, // Not meant to equip, but catalog needed for parts + `ipfs:weapon/full/${maskAssetsFull[1]}`, + [partIdForMaskCatalog2, partIdForHorns2, partIdForEars2], + ); + await mask.addEquippableAssetEntry( + maskAssetsFull[2], + 0, // Not meant to equip + catalog.address, // Not meant to equip, but catalog needed for parts + `ipfs:weapon/full/${maskAssetsFull[2]}`, + [partIdForMaskCatalog3, partIdForHorns1, partIdForEars2], + ); + await mask.addEquippableAssetEntry( + maskAssetsFull[3], + 0, // Not meant to equip + catalog.address, // Not meant to equip, but catalog needed for parts + `ipfs:weapon/full/${maskAssetsFull[3]}`, + [partIdForMaskCatalog2, partIdForHorns2, partIdForEars1], + ); + + // Assets for equipping view, also composed with fixed parts + await mask.addEquippableAssetEntry( + maskAssetsEquip[0], + maskpableGroupId, + catalog.address, + `ipfs:weapon/equip/${maskAssetsEquip[0]}`, + [partIdForMaskCatalog1, partIdForHorns1, partIdForEars1], + ); + + // Assets for equipping view, also composed with fixed parts + await mask.addEquippableAssetEntry( + maskAssetsEquip[1], + maskpableGroupId, + catalog.address, + `ipfs:weapon/equip/${maskAssetsEquip[1]}`, + [partIdForMaskCatalog2, partIdForHorns2, partIdForEars2], + ); + + // Assets for equipping view, also composed with fixed parts + await mask.addEquippableAssetEntry( + maskAssetsEquip[2], + maskpableGroupId, + catalog.address, + `ipfs:weapon/equip/${maskAssetsEquip[2]}`, + [partIdForMaskCatalog3, partIdForHorns1, partIdForEars2], + ); + + // Assets for equipping view, also composed with fixed parts + await mask.addEquippableAssetEntry( + maskAssetsEquip[3], + maskpableGroupId, + catalog.address, + `ipfs:weapon/equip/${maskAssetsEquip[3]}`, + [partIdForMaskCatalog2, partIdForHorns2, partIdForEars1], + ); + + // Can be equipped into neons + await mask.setValidParentForEquippableGroup(maskpableGroupId, neon.address, partIdForMask); + + // Add 2 assets to each weapon, one full, one for equip + // There are 10 weapon tokens for 4 unique assets so we use % + for (let i = 0; i < masks.length; i++) { + await mask.addAssetToToken(masks[i], maskAssetsFull[i % uniqueMasks], 0); + await mask.addAssetToToken(masks[i], maskAssetsEquip[i % uniqueMasks], 0); + await mask.connect(addrs[i % 3]).acceptAsset(masks[i], 0, maskAssetsFull[i % uniqueMasks]); + await mask.connect(addrs[i % 3]).acceptAsset(masks[i], 0, maskAssetsEquip[i % uniqueMasks]); + } + } +} + +async function partsFixture() { + const baseSymbol = 'NCB'; + const baseType = 'mixed'; + + const baseFactory = await ethers.getContractFactory('CatalogMock'); + const equipFactory = await ethers.getContractFactory('EquippableTokenMock'); + const viewFactory = await ethers.getContractFactory('EquipRenderUtils'); + + // Catalog + const catalog = await baseFactory.deploy(baseSymbol, baseType); + await catalog.deployed(); + + // Neon token + const neon = await equipFactory.deploy(); + await neon.deployed(); + + // Weapon + const mask = await equipFactory.deploy(); + await mask.deployed(); + + // View + const view = await viewFactory.deploy(); + await view.deployed(); + + await setupContextForParts(catalog, neon, mask); + return { catalog, neon, mask, view }; +} + +// The general idea is having these tokens: Neon and Mask +// Masks can be equipped into Neons. +// All use a single catalog. +// Neon will use an asset per token, which uses fixed parts to compose the body +// Mask will have 2 assets per weapon, one for full view, one for equipping. Both are composed using fixed parts +describe('EquippableTokenMock with Parts', async () => { + let catalog: CatalogMock; + let neon: EquippableTokenMock; + let mask: EquippableTokenMock; + let view: EquipRenderUtils; + let addrs: SignerWithAddress[]; + + beforeEach(async function () { + [, ...addrs] = await ethers.getSigners(); + ({ catalog, neon, mask, view } = await loadFixture(partsFixture)); + }); + + describe('Equip', async function () { + it('can equip weapon', async function () { + // Weapon is child on index 0, background on index 1 + const childIndex = 0; + const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + neon + .connect(addrs[0]) + .equip([neons[0], childIndex, neonResIds[0], partIdForMask, weaponResId]), + ) + .to.emit(neon, 'ChildAssetEquipped') + .withArgs(neons[0], neonResIds[0], partIdForMask, masks[0], mask.address, weaponResId); + + // All part slots are included on the response: + const expectedSlots = [bn(partIdForMask)]; + const expectedEquips = [[bn(neonResIds[0]), bn(weaponResId), bn(masks[0]), mask.address]]; + expect(await view.getEquipped(neon.address, neons[0], neonResIds[0])).to.eql([ + expectedSlots, + expectedEquips, + ]); + + // Child is marked as equipped: + expect(await neon.isChildEquipped(neons[0], mask.address, masks[0])).to.eql(true); + }); + + it('cannot equip non existing child in slot', async function () { + // Weapon is child on index 0 + const badChildIndex = 3; + const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + neon + .connect(addrs[0]) + .equip([neons[0], badChildIndex, neonResIds[0], partIdForMask, weaponResId]), + ).to.be.reverted; // Bad index + }); + }); + + describe('Compose', async function () { + it('can compose all parts for neon', async function () { + const childIndex = 0; + const weaponResId = maskAssetsEquip[0]; // This asset is assigned to weapon first weapon + await neon + .connect(addrs[0]) + .equip([neons[0], childIndex, neonResIds[0], partIdForMask, weaponResId]); + + const expectedFixedParts = [ + [ + bn(partIdForHead1), // partId + 1, // z + 'ipfs://head1.png', // metadataURI + ], + [ + bn(partIdForBody1), // partId + 1, // z + 'ipfs://body1.png', // metadataURI + ], + [ + bn(partIdForHair1), // partId + 2, // z + 'ipfs://hair1.png', // metadataURI + ], + ]; + const expectedSlotParts = [ + [ + bn(partIdForMask), // partId + bn(maskAssetsEquip[0]), // childAssetId + 2, // z + mask.address, // childAddress + bn(masks[0]), // childTokenId + 'ipfs:weapon/equip/5', // childAssetMetadata + '', // partMetadata + ], + ]; + const allAssets = await view.composeEquippables(neon.address, neons[0], neonResIds[0]); + expect(allAssets).to.eql([ + 'ipfs:neonRes/1', // metadataURI + bn(0), // equippableGroupId + catalog.address, // baseAddress, + expectedFixedParts, + expectedSlotParts, + ]); + }); + + it('can compose all parts for mask', async function () { + const expectedFixedParts = [ + [ + bn(partIdForMaskCatalog1), // partId + 3, // z + 'ipfs://maskCatalog1.png', // metadataURI + ], + [ + bn(partIdForHorns1), // partId + 5, // z + 'ipfs://horn1.png', // metadataURI + ], + [ + bn(partIdForEars1), // partId + 4, // z + 'ipfs://ears1.png', // metadataURI + ], + ]; + const allAssets = await view.composeEquippables(mask.address, masks[0], maskAssetsEquip[0]); + expect(allAssets).to.eql([ + `ipfs:weapon/equip/${maskAssetsEquip[0]}`, // metadataURI + bn(maskpableGroupId), // equippableGroupId + catalog.address, // baseAddress + expectedFixedParts, + [], + ]); + }); + + it('cannot compose equippables for neon with not associated asset', async function () { + const wrongResId = maskAssetsEquip[1]; + await expect( + view.composeEquippables(mask.address, masks[0], wrongResId), + ).to.be.revertedWithCustomError(mask, 'TokenDoesNotHaveAsset'); + }); + + it('cannot compose equippables for mask for asset with no catalog', async function () { + const noCatalogAssetId = 99; + await mask.addEquippableAssetEntry( + noCatalogAssetId, + 0, // Not meant to equip + ethers.constants.AddressZero, // Not meant to equip + `ipfs:weapon/full/customAsset.png`, + [], + ); + await mask.addAssetToToken(masks[0], noCatalogAssetId, 0); + await mask.connect(addrs[0]).acceptAsset(masks[0], 0, noCatalogAssetId); + await expect( + view.composeEquippables(mask.address, masks[0], noCatalogAssetId), + ).to.be.revertedWithCustomError(view, 'NotComposableAsset'); + }); + }); +}); + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} diff --git a/assets/eip-6220/test/equippableSlotParts.ts b/assets/eip-6220/test/equippableSlotParts.ts new file mode 100644 index 00000000000000..ff0481f8d545ca --- /dev/null +++ b/assets/eip-6220/test/equippableSlotParts.ts @@ -0,0 +1,923 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber } from 'ethers'; +import { ethers } from 'hardhat'; +import { CatalogMock, EquippableTokenMock, EquipRenderUtils } from '../typechain-types'; + +const partIdForBody = 1; +const partIdForWeapon = 2; +const partIdForWeaponGem = 3; +const partIdForBackground = 4; + +const uniqueSnakeSoldiers = 10; +const uniqueWeapons = 4; +// const uniqueWeaponGems = 2; +// const uniqueBackgrounds = 3; + +const snakeSoldiersIds: number[] = []; +const weaponsIds: number[] = []; +const weaponGemsIds: number[] = []; +const backgroundsIds: number[] = []; + +const soldierResId = 100; +const weaponAssetsFull = [1, 2, 3, 4]; // Must match the total of uniqueAssets +const weaponAssetsEquip = [5, 6, 7, 8]; // Must match the total of uniqueAssets +const weaponGemAssetFull = 101; +const weaponGemAssetEquip = 102; +const backgroundAssetId = 200; + +enum ItemType { + None, + Slot, + Fixed, +} + +let addrs: SignerWithAddress[]; + +let nextTokenId = 1; +let nextChildTokenId = 100; + +async function mint(token: EquippableTokenMock, to: string): Promise { + const tokenId = nextTokenId; + nextTokenId++; + await token['mint(address,uint256)'](to, tokenId); + return tokenId; +} + +async function nestMint(token: EquippableTokenMock, to: string, parentId: number): Promise { + const childTokenId = nextChildTokenId; + nextChildTokenId++; + await token['nestMint(address,uint256,uint256)'](to, childTokenId, parentId); + return childTokenId; +} + +async function setupContextForSlots( + catalog: CatalogMock, + soldier: EquippableTokenMock, + weapon: EquippableTokenMock, + weaponGem: EquippableTokenMock, + background: EquippableTokenMock, +) { + [, ...addrs] = await ethers.getSigners(); + + await setupCatalog(); + + await mintSnakeSoldiers(); + await mintWeapons(); + await mintWeaponGems(); + await mintBackgrounds(); + + await addAssetsToSoldier(); + await addAssetsToWeapon(); + await addAssetsToWeaponGem(); + await addAssetsToBackground(); + + return { + catalog, + soldier, + weapon, + background, + }; + + async function setupCatalog(): Promise { + const partForBody = { + itemType: ItemType.Fixed, + z: 1, + equippable: [], + metadataURI: 'genericBody.png', + }; + const partForWeapon = { + itemType: ItemType.Slot, + z: 2, + equippable: [weapon.address], + metadataURI: '', + }; + const partForWeaponGem = { + itemType: ItemType.Slot, + z: 3, + equippable: [weaponGem.address], + metadataURI: 'noGem.png', + }; + const partForBackground = { + itemType: ItemType.Slot, + z: 0, + equippable: [background.address], + metadataURI: 'noBackground.png', + }; + + await catalog.addPartList([ + { partId: partIdForBody, part: partForBody }, + { partId: partIdForWeapon, part: partForWeapon }, + { partId: partIdForWeaponGem, part: partForWeaponGem }, + { partId: partIdForBackground, part: partForBackground }, + ]); + } + + async function mintSnakeSoldiers(): Promise { + // This array is reused, so we "empty" it before + snakeSoldiersIds.length = 0; + // Using only first 3 addresses to mint + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + const newId = await mint(soldier, addrs[i % 3].address); + snakeSoldiersIds.push(newId); + } + } + + async function mintWeapons(): Promise { + // This array is reused, so we "empty" it before + weaponsIds.length = 0; + // Mint one weapon to soldier + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + const newId = await nestMint(weapon, soldier.address, snakeSoldiersIds[i]); + weaponsIds.push(newId); + await soldier + .connect(addrs[i % 3]) + .acceptChild(snakeSoldiersIds[i], 0, weapon.address, newId); + } + } + + async function mintWeaponGems(): Promise { + // This array is reused, so we "empty" it before + weaponGemsIds.length = 0; + // Mint one weapon gem for each weapon on each soldier + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + const newId = await nestMint(weaponGem, weapon.address, weaponsIds[i]); + weaponGemsIds.push(newId); + await weapon.connect(addrs[i % 3]).acceptChild(weaponsIds[i], 0, weaponGem.address, newId); + } + } + + async function mintBackgrounds(): Promise { + // This array is reused, so we "empty" it before + backgroundsIds.length = 0; + // Mint one background to soldier + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + const newId = await nestMint(background, soldier.address, snakeSoldiersIds[i]); + backgroundsIds.push(newId); + await soldier + .connect(addrs[i % 3]) + .acceptChild(snakeSoldiersIds[i], 0, background.address, newId); + } + } + + async function addAssetsToSoldier(): Promise { + await soldier.addEquippableAssetEntry(soldierResId, 0, catalog.address, 'ipfs:soldier/', [ + partIdForBody, + partIdForWeapon, + partIdForBackground, + ]); + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + await soldier.addAssetToToken(snakeSoldiersIds[i], soldierResId, 0); + await soldier.connect(addrs[i % 3]).acceptAsset(snakeSoldiersIds[i], 0, soldierResId); + } + } + + async function addAssetsToWeapon(): Promise { + const equippableGroupId = 1; // Assets to equip will both use this + + for (let i = 0; i < weaponAssetsFull.length; i++) { + await weapon.addEquippableAssetEntry( + weaponAssetsFull[i], + 0, // Not meant to equip + ethers.constants.AddressZero, // Not meant to equip + `ipfs:weapon/full/${weaponAssetsFull[i]}`, + [], + ); + } + for (let i = 0; i < weaponAssetsEquip.length; i++) { + await weapon.addEquippableAssetEntry( + weaponAssetsEquip[i], + equippableGroupId, + catalog.address, + `ipfs:weapon/equip/${weaponAssetsEquip[i]}`, + [partIdForWeaponGem], + ); + } + + // Can be equipped into snakeSoldiers + await weapon.setValidParentForEquippableGroup( + equippableGroupId, + soldier.address, + partIdForWeapon, + ); + + // Add 2 assets to each weapon, one full, one for equip + // There are 10 weapon tokens for 4 unique assets so we use % + for (let i = 0; i < weaponsIds.length; i++) { + await weapon.addAssetToToken(weaponsIds[i], weaponAssetsFull[i % uniqueWeapons], 0); + await weapon.addAssetToToken(weaponsIds[i], weaponAssetsEquip[i % uniqueWeapons], 0); + await weapon + .connect(addrs[i % 3]) + .acceptAsset(weaponsIds[i], 0, weaponAssetsFull[i % uniqueWeapons]); + await weapon + .connect(addrs[i % 3]) + .acceptAsset(weaponsIds[i], 0, weaponAssetsEquip[i % uniqueWeapons]); + } + } + + async function addAssetsToWeaponGem(): Promise { + const equippableGroupId = 1; // Assets to equip will use this + await weaponGem.addEquippableAssetEntry( + weaponGemAssetFull, + 0, // Not meant to equip + ethers.constants.AddressZero, // Not meant to equip + 'ipfs:weagponGem/full/', + [], + ); + await weaponGem.addEquippableAssetEntry( + weaponGemAssetEquip, + equippableGroupId, + catalog.address, + 'ipfs:weagponGem/equip/', + [], + ); + await weaponGem.setValidParentForEquippableGroup( + // Can be equipped into weapons + equippableGroupId, + weapon.address, + partIdForWeaponGem, + ); + + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + await weaponGem.addAssetToToken(weaponGemsIds[i], weaponGemAssetFull, 0); + await weaponGem.addAssetToToken(weaponGemsIds[i], weaponGemAssetEquip, 0); + await weaponGem.connect(addrs[i % 3]).acceptAsset(weaponGemsIds[i], 0, weaponGemAssetFull); + await weaponGem.connect(addrs[i % 3]).acceptAsset(weaponGemsIds[i], 0, weaponGemAssetEquip); + } + } + + async function addAssetsToBackground(): Promise { + const equippableGroupId = 1; // Assets to equip will use this + await background.addEquippableAssetEntry( + backgroundAssetId, + equippableGroupId, + catalog.address, + 'ipfs:background/', + [], + ); + // Can be equipped into snakeSoldiers + await background.setValidParentForEquippableGroup( + equippableGroupId, + soldier.address, + partIdForBackground, + ); + + for (let i = 0; i < uniqueSnakeSoldiers; i++) { + await background.addAssetToToken(backgroundsIds[i], backgroundAssetId, 0); + await background.connect(addrs[i % 3]).acceptAsset(backgroundsIds[i], 0, backgroundAssetId); + } + } +} + +async function slotsFixture() { + const catalogSymbol = 'SSB'; + const catalogType = 'mixed'; + + const catalogFactory = await ethers.getContractFactory('CatalogMock'); + const equipFactory = await ethers.getContractFactory('EquippableTokenMock'); + const viewFactory = await ethers.getContractFactory('EquipRenderUtils'); + + // View + const view = await viewFactory.deploy(); + await view.deployed(); + + // Catalog + const catalog = await catalogFactory.deploy(catalogSymbol, catalogType); + await catalog.deployed(); + + // Soldier token + const soldier = await equipFactory.deploy(); + await soldier.deployed(); + + // Weapon + const weapon = await equipFactory.deploy(); + await weapon.deployed(); + + // Weapon Gem + const weaponGem = await equipFactory.deploy(); + await weaponGem.deployed(); + + // Background + const background = await equipFactory.deploy(); + await background.deployed(); + + await setupContextForSlots(catalog, soldier, weapon, weaponGem, background); + + return { catalog, soldier, weapon, weaponGem, background, view }; +} + +// The general idea is having these tokens: Soldier, Weapon, WeaponGem and Background. +// Weapon and Background can be equipped into Soldier. WeaponGem can be equipped into Weapon +// All use a single catalog. +// Soldier will use a single enumerated fixed asset for simplicity +// Weapon will have 2 assets per weapon, one for full view, one for equipping +// Background will have a single asset for each, it can be used as full view and to equip +// Weapon Gems will have 2 enumerated assets, one for full view, one for equipping. +describe('EquippableTokenMock with Slots', async () => { + let catalog: CatalogMock; + let soldier: EquippableTokenMock; + let weapon: EquippableTokenMock; + let weaponGem: EquippableTokenMock; + let background: EquippableTokenMock; + let view: EquipRenderUtils; + + let addrs: SignerWithAddress[]; + + beforeEach(async function () { + [, ...addrs] = await ethers.getSigners(); + ({ catalog, soldier, weapon, weaponGem, background, view } = await loadFixture(slotsFixture)); + }); + + it('can support IEquippable', async function () { + expect(await soldier.supportsInterface('0x28bc9ae4')).to.equal(true); + }); + + describe('Validations', async function () { + it('can validate equips of weapons into snakeSoldiers', async function () { + // This asset is not equippable + expect( + await weapon.canTokenBeEquippedWithAssetIntoSlot( + soldier.address, + weaponsIds[0], + weaponAssetsFull[0], + partIdForWeapon, + ), + ).to.eql(false); + + // This asset is equippable into weapon part + expect( + await weapon.canTokenBeEquippedWithAssetIntoSlot( + soldier.address, + weaponsIds[0], + weaponAssetsEquip[0], + partIdForWeapon, + ), + ).to.eql(true); + + // This asset is NOT equippable into weapon gem part + expect( + await weapon.canTokenBeEquippedWithAssetIntoSlot( + soldier.address, + weaponsIds[0], + weaponAssetsEquip[0], + partIdForWeaponGem, + ), + ).to.eql(false); + }); + + it('can validate equips of weapon gems into weapons', async function () { + // This asset is not equippable + expect( + await weaponGem.canTokenBeEquippedWithAssetIntoSlot( + weapon.address, + weaponGemsIds[0], + weaponGemAssetFull, + partIdForWeaponGem, + ), + ).to.eql(false); + + // This asset is equippable into weapon gem slot + expect( + await weaponGem.canTokenBeEquippedWithAssetIntoSlot( + weapon.address, + weaponGemsIds[0], + weaponGemAssetEquip, + partIdForWeaponGem, + ), + ).to.eql(true); + + // This asset is NOT equippable into background slot + expect( + await weaponGem.canTokenBeEquippedWithAssetIntoSlot( + weapon.address, + weaponGemsIds[0], + weaponGemAssetEquip, + partIdForBackground, + ), + ).to.eql(false); + }); + + it('can validate equips of backgrounds into snakeSoldiers', async function () { + // This asset is equippable into background slot + expect( + await background.canTokenBeEquippedWithAssetIntoSlot( + soldier.address, + backgroundsIds[0], + backgroundAssetId, + partIdForBackground, + ), + ).to.eql(true); + + // This asset is NOT equippable into weapon slot + expect( + await background.canTokenBeEquippedWithAssetIntoSlot( + soldier.address, + backgroundsIds[0], + backgroundAssetId, + partIdForWeapon, + ), + ).to.eql(false); + }); + }); + + describe('Equip', async function () { + it('can equip weapon', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await equipWeaponAndCheckFromAddress(soldierOwner, childIndex, weaponResId); + }); + + it('can equip weapon if approved', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const approved = addrs[1]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier.connect(soldierOwner).approve(approved.address, snakeSoldiersIds[0]); + await equipWeaponAndCheckFromAddress(approved, childIndex, weaponResId); + }); + + it('can equip weapon if approved for all', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const approved = addrs[1]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier.connect(soldierOwner).setApprovalForAll(approved.address, true); + await equipWeaponAndCheckFromAddress(approved, childIndex, weaponResId); + }); + + it('can equip weapon and background', async function () { + // Weapon is child on index 0, background on index 1 + const weaponChildIndex = 0; + const backgroundChildIndex = 1; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], weaponChildIndex, soldierResId, partIdForWeapon, weaponResId]); + await soldier + .connect(addrs[0]) + .equip([ + snakeSoldiersIds[0], + backgroundChildIndex, + soldierResId, + partIdForBackground, + backgroundAssetId, + ]); + + const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)]; + const expectedEquips = [ + [bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weapon.address], + [bn(soldierResId), bn(backgroundAssetId), bn(backgroundsIds[0]), background.address], + ]; + expect(await view.getEquipped(soldier.address, snakeSoldiersIds[0], soldierResId)).to.eql([ + expectedSlots, + expectedEquips, + ]); + + // Children are marked as equipped: + expect( + await soldier.isChildEquipped(snakeSoldiersIds[0], weapon.address, weaponsIds[0]), + ).to.eql(true); + expect( + await soldier.isChildEquipped(snakeSoldiersIds[0], background.address, backgroundsIds[0]), + ).to.eql(true); + }); + + it('cannot equip non existing child in slot (weapon in background)', async function () { + // Weapon is child on index 0, background on index 1 + const badChildIndex = 3; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], badChildIndex, soldierResId, partIdForWeapon, weaponResId]), + ).to.be.reverted; // Bad index + }); + + it('cannot set a valid equippable group with id 0', async function () { + const equippableGroupId = 0; + // The malicious child indicates it can be equipped into soldier: + await expect( + weaponGem.setValidParentForEquippableGroup( + equippableGroupId, + soldier.address, + partIdForWeaponGem, + ), + ).to.be.revertedWithCustomError(weaponGem, 'IdZeroForbidden'); + }); + + it('cannot set a valid equippable group with part id 0', async function () { + const equippableGroupId = 1; + const partId = 0; + // The malicious child indicates it can be equipped into soldier: + await expect( + weaponGem.setValidParentForEquippableGroup(equippableGroupId, soldier.address, partId), + ).to.be.revertedWithCustomError(weaponGem, 'IdZeroForbidden'); + }); + + it('cannot equip into a slot not set on the parent asset (gem into soldier)', async function () { + const soldierOwner = addrs[0]; + const soldierId = snakeSoldiersIds[0]; + const childIndex = 2; + + const newWeaponGemId = await nestMint(weaponGem, soldier.address, soldierId); + await soldier + .connect(soldierOwner) + .acceptChild(soldierId, 0, weaponGem.address, newWeaponGemId); + + // Add assets to weapon + await weaponGem.addAssetToToken(newWeaponGemId, weaponGemAssetFull, 0); + await weaponGem.addAssetToToken(newWeaponGemId, weaponGemAssetEquip, 0); + await weaponGem.connect(soldierOwner).acceptAsset(newWeaponGemId, 0, weaponGemAssetFull); + await weaponGem.connect(soldierOwner).acceptAsset(newWeaponGemId, 0, weaponGemAssetEquip); + + // The malicious child indicates it can be equipped into soldier: + await weaponGem.setValidParentForEquippableGroup( + 1, // equippableGroupId for gems + soldier.address, + partIdForWeaponGem, + ); + + // Weapon is child on index 0, background on index 1 + await expect( + soldier + .connect(addrs[0]) + .equip([soldierId, childIndex, soldierResId, partIdForWeaponGem, weaponGemAssetEquip]), + ).to.be.revertedWithCustomError(soldier, 'TargetAssetCannotReceiveSlot'); + }); + + it('cannot equip wrong child in slot (weapon in background)', async function () { + // Weapon is child on index 0, background on index 1 + const backgroundChildIndex = 1; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + soldier + .connect(addrs[0]) + .equip([ + snakeSoldiersIds[0], + backgroundChildIndex, + soldierResId, + partIdForWeapon, + weaponResId, + ]), + ).to.be.revertedWithCustomError(soldier, 'TokenCannotBeEquippedWithAssetIntoSlot'); + }); + + it('cannot equip child in wrong slot (weapon in background)', async function () { + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForBackground, weaponResId]), + ).to.be.revertedWithCustomError(soldier, 'TokenCannotBeEquippedWithAssetIntoSlot'); + }); + + it('cannot equip child with wrong asset (weapon in background)', async function () { + const childIndex = 0; + await expect( + soldier + .connect(addrs[0]) + .equip([ + snakeSoldiersIds[0], + childIndex, + soldierResId, + partIdForWeapon, + backgroundAssetId, + ]), + ).to.be.revertedWithCustomError(soldier, 'TokenCannotBeEquippedWithAssetIntoSlot'); + }); + + it('cannot equip if not owner', async function () { + // Weapon is child on index 0, background on index 1 + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await expect( + soldier + .connect(addrs[1]) // Owner is addrs[0] + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]), + ).to.be.revertedWithCustomError(soldier, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot equip 2 children into the same slot', async function () { + // Weapon is child on index 0, background on index 1 + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + const weaponAssetIndex = 3; + await mintWeaponToSoldier(addrs[0], snakeSoldiersIds[0], weaponAssetIndex); + + const newWeaponChildIndex = 2; + const newWeaponResId = weaponAssetsEquip[weaponAssetIndex]; + await expect( + soldier + .connect(addrs[0]) + .equip([ + snakeSoldiersIds[0], + newWeaponChildIndex, + soldierResId, + partIdForWeapon, + newWeaponResId, + ]), + ).to.be.revertedWithCustomError(soldier, 'SlotAlreadyUsed'); + }); + + it('cannot equip if not intented on catalog', async function () { + // Weapon is child on index 0, background on index 1 + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + + // Remove equippable addresses for part. + await catalog.resetEquippableAddresses(partIdForWeapon); + await expect( + soldier + .connect(addrs[0]) // Owner is addrs[0] + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]), + ).to.be.revertedWithCustomError(soldier, 'EquippableEquipNotAllowedByCatalog'); + }); + }); + + describe('Unequip', async function () { + it('can unequip', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + + await soldier + .connect(soldierOwner) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await unequipWeaponAndCheckFromAddress(soldierOwner); + }); + + it('can unequip if approved', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + const approved = addrs[1]; + + await soldier + .connect(soldierOwner) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await soldier.connect(soldierOwner).approve(approved.address, snakeSoldiersIds[0]); + await unequipWeaponAndCheckFromAddress(approved); + }); + + it('can unequip if approved for all', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + const approved = addrs[1]; + + await soldier + .connect(soldierOwner) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await soldier.connect(soldierOwner).setApprovalForAll(approved.address, true); + await unequipWeaponAndCheckFromAddress(approved); + }); + + it('cannot unequip if not equipped', async function () { + await expect( + soldier.connect(addrs[0]).unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon), + ).to.be.revertedWithCustomError(soldier, 'NotEquipped'); + }); + + it('cannot unequip if not owner', async function () { + // Weapon is child on index 0, background on index 1 + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await expect( + soldier.connect(addrs[1]).unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon), + ).to.be.revertedWithCustomError(soldier, 'ERC721NotApprovedOrOwner'); + }); + }); + + describe('Transfer equipped', async function () { + it('can unequip and transfer child', async function () { + // Weapon is child on index 0, background on index 1 + const soldierOwner = addrs[0]; + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + + await soldier + .connect(soldierOwner) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await unequipWeaponAndCheckFromAddress(soldierOwner); + await soldier + .connect(soldierOwner) + .transferChild( + snakeSoldiersIds[0], + soldierOwner.address, + 0, + childIndex, + weapon.address, + weaponsIds[0], + false, + '0x', + ); + }); + + it('child transfer fails if child is equipped', async function () { + const soldierOwner = addrs[0]; + // Weapon is child on index 0 + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + await expect( + soldier + .connect(soldierOwner) + .transferChild( + snakeSoldiersIds[0], + soldierOwner.address, + 0, + childIndex, + weapon.address, + weaponsIds[0], + false, + '0x', + ), + ).to.be.revertedWithCustomError(weapon, 'MustUnequipFirst'); + }); + }); + + describe('Compose', async function () { + it('can compose equippables for soldier', async function () { + const childIndex = 0; + const weaponResId = weaponAssetsEquip[0]; // This asset is assigned to weapon first weapon + await soldier + .connect(addrs[0]) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]); + + const expectedFixedParts = [ + [ + bn(partIdForBody), // partId + 1, // z + 'genericBody.png', // metadataURI + ], + ]; + const expectedSlotParts = [ + [ + bn(partIdForWeapon), // partId + bn(weaponAssetsEquip[0]), // childAssetId + 2, // z + weapon.address, // childAddress + bn(weaponsIds[0]), // childTokenId + 'ipfs:weapon/equip/5', // childAssetMetadata + '', // partMetadata + ], + [ + // Nothing on equipped on background slot: + bn(partIdForBackground), // partId + bn(0), // childAssetId + 0, // z + ethers.constants.AddressZero, // childAddress + bn(0), // childTokenId + '', // childAssetMetadata + 'noBackground.png', // partMetadata + ], + ]; + const allAssets = await view.composeEquippables( + soldier.address, + snakeSoldiersIds[0], + soldierResId, + ); + expect(allAssets).to.eql([ + 'ipfs:soldier/', // metadataURI + bn(0), // equippableGroupId + catalog.address, // catalogAddress + expectedFixedParts, + expectedSlotParts, + ]); + }); + + it('can compose equippables for simple asset', async function () { + const allAssets = await view.composeEquippables( + background.address, + backgroundsIds[0], + backgroundAssetId, + ); + expect(allAssets).to.eql([ + 'ipfs:background/', // metadataURI + bn(1), // equippableGroupId + catalog.address, // catalogAddress, + [], + [], + ]); + }); + + it('cannot compose equippables for soldier with not associated asset', async function () { + const wrongResId = weaponAssetsEquip[1]; + await expect( + view.composeEquippables(weapon.address, weaponsIds[0], wrongResId), + ).to.be.revertedWithCustomError(weapon, 'TokenDoesNotHaveAsset'); + }); + }); + + async function equipWeaponAndCheckFromAddress( + from: SignerWithAddress, + childIndex: number, + weaponResId: number, + ): Promise { + await expect( + soldier + .connect(from) + .equip([snakeSoldiersIds[0], childIndex, soldierResId, partIdForWeapon, weaponResId]), + ) + .to.emit(soldier, 'ChildAssetEquipped') + .withArgs( + snakeSoldiersIds[0], + soldierResId, + partIdForWeapon, + weaponsIds[0], + weapon.address, + weaponAssetsEquip[0], + ); + // All part slots are included on the response: + const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)]; + // If a slot has nothing equipped, it returns an empty equip: + const expectedEquips = [ + [bn(soldierResId), bn(weaponResId), bn(weaponsIds[0]), weapon.address], + [bn(0), bn(0), bn(0), ethers.constants.AddressZero], + ]; + expect(await view.getEquipped(soldier.address, snakeSoldiersIds[0], soldierResId)).to.eql([ + expectedSlots, + expectedEquips, + ]); + + // Child is marked as equipped: + expect( + await soldier.isChildEquipped(snakeSoldiersIds[0], weapon.address, weaponsIds[0]), + ).to.eql(true); + } + + async function unequipWeaponAndCheckFromAddress(from: SignerWithAddress): Promise { + await expect(soldier.connect(from).unequip(snakeSoldiersIds[0], soldierResId, partIdForWeapon)) + .to.emit(soldier, 'ChildAssetUnequipped') + .withArgs( + snakeSoldiersIds[0], + soldierResId, + partIdForWeapon, + weaponsIds[0], + weapon.address, + weaponAssetsEquip[0], + ); + + const expectedSlots = [bn(partIdForWeapon), bn(partIdForBackground)]; + // If a slot has nothing equipped, it returns an empty equip: + const expectedEquips = [ + [bn(0), bn(0), bn(0), ethers.constants.AddressZero], + [bn(0), bn(0), bn(0), ethers.constants.AddressZero], + ]; + expect(await view.getEquipped(soldier.address, snakeSoldiersIds[0], soldierResId)).to.eql([ + expectedSlots, + expectedEquips, + ]); + + // Child is marked as not equipped: + expect( + await soldier.isChildEquipped(snakeSoldiersIds[0], weapon.address, weaponsIds[0]), + ).to.eql(false); + } + + async function mintWeaponToSoldier( + soldierOwner: SignerWithAddress, + soldierId: number, + assetIndex: number, + ): Promise { + // Mint another weapon to the soldier and accept it + const newWeaponId = await nestMint(weapon, soldier.address, soldierId); + await soldier.connect(soldierOwner).acceptChild(soldierId, 0, weapon.address, newWeaponId); + + // Add assets to weapon + await weapon.addAssetToToken(newWeaponId, weaponAssetsFull[assetIndex], 0); + await weapon.addAssetToToken(newWeaponId, weaponAssetsEquip[assetIndex], 0); + await weapon.connect(soldierOwner).acceptAsset(newWeaponId, 0, weaponAssetsFull[assetIndex]); + await weapon.connect(soldierOwner).acceptAsset(newWeaponId, 0, weaponAssetsEquip[assetIndex]); + + return newWeaponId; + } +}); + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} diff --git a/assets/eip-6220/test/multiasset.ts b/assets/eip-6220/test/multiasset.ts new file mode 100644 index 00000000000000..0d511556bacd41 --- /dev/null +++ b/assets/eip-6220/test/multiasset.ts @@ -0,0 +1,670 @@ +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { + EquippableTokenMock, + ERC721ReceiverMock, + MultiAssetRenderUtils, + NonReceiverMock +} from '../typechain-types'; + +describe('MultiAsset', async () => { + let token: EquippableTokenMock; + let renderUtils: MultiAssetRenderUtils; + let nonReceiver: NonReceiverMock; + let receiver721: ERC721ReceiverMock; + + let owner: SignerWithAddress; + let addrs: SignerWithAddress[]; + + const metaURIDefault = 'metaURI'; + + beforeEach(async () => { + [owner, ...addrs] = await ethers.getSigners(); + + const equppableFactory = await ethers.getContractFactory('EquippableTokenMock'); + token = await equppableFactory.deploy(); + await token.deployed(); + + const renderFactory = await ethers.getContractFactory('MultiAssetRenderUtils'); + renderUtils = await renderFactory.deploy(); + await renderUtils.deployed(); + }); + + describe('ERC165 check', async function () { + it('can support IERC165', async function () { + expect(await token.supportsInterface('0x01ffc9a7')).to.equal(true); + }); + + it('can support IERC721', async function () { + expect(await token.supportsInterface('0x80ac58cd')).to.equal(true); + }); + + it('can support IMultiAsset', async function () { + expect(await token.supportsInterface('0xd1526708')).to.equal(true); + }); + + it('cannot support other interfaceId', async function () { + expect(await token.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('Check OnReceived ERC721 and Multiasset', async function () { + it('Revert on transfer to non onERC721/onMultiasset implementer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const NonReceiver = await ethers.getContractFactory('NonReceiverMock'); + nonReceiver = await NonReceiver.deploy(); + await nonReceiver.deployed(); + + await expect( + token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, nonReceiver.address, 1), + ).to.be.revertedWithCustomError(token, 'ERC721TransferToNonReceiverImplementer'); + }); + + it('onERC721Received callback on transfer', async function () { + const tokenId = 1; + await token.mint(owner.address, tokenId); + + const ERC721Receiver = await ethers.getContractFactory('ERC721ReceiverMock'); + receiver721 = await ERC721Receiver.deploy(); + await receiver721.deployed(); + + await token + .connect(owner) + ['safeTransferFrom(address,address,uint256)'](owner.address, receiver721.address, 1); + expect(await token.ownerOf(1)).to.equal(receiver721.address); + }); + }); + + describe('Asset storage', async function () { + it('can add asset', async function () { + const id = 10; + + await expect(token.addAssetEntry(id, metaURIDefault)).to.emit(token, 'AssetSet').withArgs(id); + }); + + it('cannot get non existing asset', async function () { + const tokenId = 1; + const resId = 10; + await token.mint(owner.address, tokenId); + await expect(token.getAssetMetadata(tokenId, resId)).to.be.revertedWithCustomError( + token, + 'TokenDoesNotHaveAsset', + ); + }); + + it('cannot add asset entry if not issuer', async function () { + const id = 10; + await expect(token.connect(addrs[1]).addAssetEntry(id, metaURIDefault)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('can set and get issuer', async function () { + const newIssuerAddr = addrs[1].address; + expect(await token.getIssuer()).to.equal(owner.address); + + await token.setIssuer(newIssuerAddr); + expect(await token.getIssuer()).to.equal(newIssuerAddr); + }); + + it('cannot set issuer if not issuer', async function () { + const newIssuer = addrs[1]; + await expect(token.connect(newIssuer).setIssuer(newIssuer.address)).to.be.revertedWith( + 'RMRK: Only issuer', + ); + }); + + it('cannot overwrite asset', async function () { + const id = 10; + + await token.addAssetEntry(id, metaURIDefault); + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWithCustomError( + token, + 'AssetAlreadyExists', + ); + }); + + it('cannot add asset with id 0', async function () { + const id = ethers.utils.hexZeroPad('0x0', 8); + + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWithCustomError( + token, + 'IdZeroForbidden', + ); + }); + + it('cannot add same asset twice', async function () { + const id = 10; + + await expect(token.addAssetEntry(id, metaURIDefault)).to.emit(token, 'AssetSet').withArgs(id); + + await expect(token.addAssetEntry(id, metaURIDefault)).to.be.revertedWithCustomError( + token, + 'AssetAlreadyExists', + ); + }); + }); + + describe('Adding assets', async function () { + it('can add asset to token', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.emit(token, 'AssetAddedToToken'); + await expect(token.addAssetToToken(tokenId, resId2, 0)).to.emit(token, 'AssetAddedToToken'); + + const pendingIds = await token.getPendingAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, pendingIds)).to.be.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot add non existing asset to token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.be.revertedWithCustomError( + token, + 'NoAssetMatchingId', + ); + }); + + it('can add asset to non existing token and it is pending when minted', async function () { + const resId = 1; + const tokenId = 1; + await addAssets([resId]); + + await token.addAssetToToken(tokenId, resId, 0); + await token.mint(owner.address, tokenId); + expect(await token.getPendingAssets(tokenId)).to.eql([ethers.BigNumber.from(resId)]); + }); + + it('cannot add asset twice to the same token', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect( + token.addAssetToToken(tokenId, ethers.BigNumber.from(resId), 0), + ).to.be.revertedWithCustomError(token, 'AssetAlreadyExists'); + }); + + it('cannot add too many assets to the same token', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + for (let i = 1; i <= 128; i++) { + await addAssets([i]); + await token.addAssetToToken(tokenId, i, 0); + } + + // Now it's full, next should fail + const resId = 129; + await addAssets([resId]); + await expect(token.addAssetToToken(tokenId, resId, 0)).to.be.revertedWithCustomError( + token, + 'MaxPendingAssetsReached', + ); + }); + + it('can add same asset to 2 different tokens', async function () { + const resId = 1; + const tokenId1 = 1; + const tokenId2 = 2; + + await token.mint(owner.address, tokenId1); + await token.mint(owner.address, tokenId2); + await addAssets([resId]); + await token.addAssetToToken(tokenId1, resId, 0); + await token.addAssetToToken(tokenId2, resId, 0); + }); + }); + + describe('Accepting assets', async function () { + it('can accept asset if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept asset if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept asset if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkAcceptFromAddress(approved, tokenId); + }); + + it('can accept multiple assets', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + await expect(token.acceptAsset(tokenId, 1, resId2)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId2, 0); + await expect(token.acceptAsset(tokenId, 0, resId)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + metaURIDefault, + ]); + }); + + it('cannot accept asset twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + }); + + it('cannot accept asset if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect( + token.connect(addrs[1]).acceptAsset(tokenId, 0, resId), + ).to.be.revertedWithCustomError(token, 'NotApprovedForAssetsOrOwner'); + }); + + it('cannot accept non existing asset', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.acceptAsset(tokenId, 0, 1)).to.be.revertedWithCustomError( + token, + 'IndexOutOfRange', + ); + }); + }); + + describe('Overwriting assets', async function () { + it('can add asset to token overwritting an existing one', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Add new asset to overwrite the first, and accept + const activeAssets = await token.getActiveAssets(tokenId); + await expect(token.addAssetToToken(tokenId, resId2, activeAssets[0])) + .to.emit(token, 'AssetAddedToToken') + .withArgs(tokenId, resId2, resId); + const pendingAssets = await token.getPendingAssets(tokenId); + + expect(await token.getAssetReplacements(tokenId, pendingAssets[0])).to.eql(activeAssets[0]); + await expect(token.acceptAsset(tokenId, 0, resId2)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId2, resId); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + // Overwrite should be gone + expect(await token.getAssetReplacements(tokenId, pendingAssets[0])).to.eql( + ethers.BigNumber.from(0), + ); + }); + + it('can overwrite non existing asset to token, it could have been deleted', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, ethers.utils.hexZeroPad('0x1', 8)); + await token.acceptAsset(tokenId, 0, resId); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + }); + }); + + describe('Rejecting assets', async function () { + it('can reject asset if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject asset if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject asset if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkRejectFromAddress(approved, tokenId); + }); + + it('can reject all assets if owner', async function () { + const { tokenOwner, tokenId } = await mintSampleToken(); + const approved = tokenOwner; + + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all assets if approved for assets', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[1]; + + await token.approveForAssets(approved.address, tokenId); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject all assets if approved for assets for all', async function () { + const { tokenId } = await mintSampleToken(); + const approved = addrs[2]; + + await token.setApprovalForAllForAssets(approved.address, true); + await checkRejectAllFromAddress(approved, tokenId); + }); + + it('can reject asset and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Will try to overwrite but we reject it + await token.addAssetToToken(tokenId, resId2, resId); + await token.rejectAsset(tokenId, 0, resId2); + + expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all assets and overwrites are cleared', async function () { + const resId = 1; + const resId2 = 2; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.acceptAsset(tokenId, 0, resId); + + // Will try to overwrite but we reject all + await token.addAssetToToken(tokenId, resId2, resId); + await token.rejectAllAssets(tokenId, 1); + + expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('can reject all pending assets at max capacity', async function () { + const tokenId = 1; + const resArr = []; + + for (let i = 1; i < 128; i++) { + resArr.push(i); + } + + await token.mint(owner.address, tokenId); + await addAssets(resArr); + + for (let i = 1; i < 128; i++) { + await token.addAssetToToken(tokenId, i, 1); + } + await token.rejectAllAssets(tokenId, 128); + + expect(await token.getAssetReplacements(1, 2)).to.eql(ethers.BigNumber.from(0)); + }); + + it('cannot reject asset twice', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await token.rejectAsset(tokenId, 0, resId); + }); + + it('cannot reject asset nor reject all if not owner', async function () { + const resId = 1; + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + + await expect( + token.connect(addrs[1]).rejectAsset(tokenId, 0, resId), + ).to.be.revertedWithCustomError(token, 'NotApprovedForAssetsOrOwner'); + await expect( + token.connect(addrs[1]).rejectAllAssets(tokenId, 1), + ).to.be.revertedWithCustomError(token, 'NotApprovedForAssetsOrOwner'); + }); + + it('cannot reject non existing asset', async function () { + const tokenId = 1; + + await token.mint(owner.address, tokenId); + await expect(token.rejectAsset(tokenId, 0, 1)).to.be.revertedWithCustomError( + token, + 'IndexOutOfRange', + ); + }); + }); + + describe('Priorities', async function () { + it('can set and get priorities', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + + expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([0, 1]); + await expect(token.setPriority(tokenId, [2, 1])) + .to.emit(token, 'AssetPrioritySet') + .withArgs(tokenId); + expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([2, 1]); + }); + + it('cannot set priorities for non owned token', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + await expect( + token.connect(addrs[1]).setPriority(tokenId, [2, 1]), + ).to.be.revertedWithCustomError(token, 'NotApprovedForAssetsOrOwner'); + }); + + it('cannot set different number of priorities', async function () { + const tokenId = 1; + await addAssetsToToken(tokenId); + await expect(token.setPriority(tokenId, [1])).to.be.revertedWithCustomError( + token, + 'BadPriorityListLength', + ); + await expect(token.setPriority(tokenId, [2, 1, 3])).to.be.revertedWithCustomError( + token, + 'BadPriorityListLength', + ); + }); + + it('cannot set priorities for non existing token', async function () { + const tokenId = 1; + await expect(token.connect(addrs[1]).setPriority(tokenId, [])).to.be.revertedWithCustomError( + token, + 'ERC721InvalidTokenId', + ); + }); + }); + + describe('Approval Cleaning', async function () { + it('cleans token and assets approvals on transfer', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const newOwner = addrs[2]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForAssets(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForAssets(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner).transfer(newOwner.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(ethers.constants.AddressZero); + expect(await token.getApprovedForAssets(tokenId)).to.eql(ethers.constants.AddressZero); + }); + + it('cleans token and assets approvals on burn', async function () { + const tokenId = 1; + const tokenOwner = addrs[1]; + const approved = addrs[3]; + await token.mint(tokenOwner.address, tokenId); + await token.connect(tokenOwner).approve(approved.address, tokenId); + await token.connect(tokenOwner).approveForAssets(approved.address, tokenId); + + expect(await token.getApproved(tokenId)).to.eql(approved.address); + expect(await token.getApprovedForAssets(tokenId)).to.eql(approved.address); + + await token.connect(tokenOwner)['burn(uint256)'](tokenId); + + await expect(token.getApproved(tokenId)).to.be.revertedWithCustomError( + token, + 'ERC721InvalidTokenId', + ); + await expect(token.getApprovedForAssets(tokenId)).to.be.revertedWithCustomError( + token, + 'ERC721InvalidTokenId', + ); + }); + }); + + async function mintSampleToken(): Promise<{ tokenOwner: SignerWithAddress; tokenId: number }> { + const tokenOwner = owner; + const tokenId = 1; + await token.mint(tokenOwner.address, tokenId); + + return { tokenOwner, tokenId }; + } + + async function addAssets(ids: number[]): Promise { + ids.forEach(async (resId) => { + await token.addAssetEntry(resId, metaURIDefault); + }); + } + + async function addAssetsToToken(tokenId: number): Promise { + const resId = 1; + const resId2 = 2; + await token.mint(owner.address, tokenId); + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + await token.acceptAsset(tokenId, 0, resId); + await token.acceptAsset(tokenId, 0, resId2); + } + + async function checkAcceptFromAddress( + accepter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + await expect(token.connect(accepter).acceptAsset(tokenId, 0, resId)) + .to.emit(token, 'AssetAccepted') + .withArgs(tokenId, resId, 0); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + + const activeIds = await token.getActiveAssets(tokenId); + expect(await renderUtils.getAssetsById(token.address, tokenId, activeIds)).to.eql([ + metaURIDefault, + ]); + } + + async function checkRejectFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + + await addAssets([resId]); + await token.addAssetToToken(tokenId, resId, 0); + + await expect(token.connect(rejecter).rejectAsset(tokenId, 0, resId)).to.emit( + token, + 'AssetRejected', + ); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + expect(await token.getActiveAssets(tokenId)).to.be.eql([]); + } + + async function checkRejectAllFromAddress( + rejecter: SignerWithAddress, + tokenId: number, + ): Promise { + const resId = 1; + const resId2 = 2; + + await addAssets([resId, resId2]); + await token.addAssetToToken(tokenId, resId, 0); + await token.addAssetToToken(tokenId, resId2, 0); + + await expect(token.connect(rejecter).rejectAllAssets(tokenId, 2)).to.emit( + token, + 'AssetRejected', + ); + + expect(await token.getPendingAssets(tokenId)).to.be.eql([]); + expect(await token.getActiveAssets(tokenId)).to.be.eql([]); + } +}); diff --git a/assets/eip-6220/test/nestable.ts b/assets/eip-6220/test/nestable.ts new file mode 100644 index 00000000000000..88a0b863f45fac --- /dev/null +++ b/assets/eip-6220/test/nestable.ts @@ -0,0 +1,1180 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber, constants } from 'ethers'; +import { ethers } from 'hardhat'; +import { EquippableTokenMock } from '../typechain-types'; + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} + +const ADDRESS_ZERO = constants.AddressZero; + +async function parentChildFixture(): Promise<{ + parent: EquippableTokenMock; + child: EquippableTokenMock; +}> { + const factory = await ethers.getContractFactory('EquippableTokenMock'); + + const parent = await factory.deploy(); + await parent.deployed(); + const child = await factory.deploy(); + await child.deployed(); + return { parent, child }; +} + +describe('NestableToken', function () { + let parent: EquippableTokenMock; + let child: EquippableTokenMock; + let owner: SignerWithAddress; + let tokenOwner: SignerWithAddress; + let addrs: SignerWithAddress[]; + + beforeEach(async function () { + [owner, tokenOwner, ...addrs] = await ethers.getSigners(); + ({ parent, child } = await loadFixture(parentChildFixture)); + }); + + describe('Minting', async function () { + it('cannot mint id 0', async function () { + const tokenId1 = 0; + await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError( + child, + 'IdZeroForbidden', + ); + }); + + it('cannot nest mint id 0', async function () { + const parentId = 1; + await child.mint(owner.address, parentId); + const childId1 = 0; + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'IdZeroForbidden'); + }); + + it('cannot mint already minted token', async function () { + const tokenId1 = 1; + await child.mint(owner.address, tokenId1); + await expect(child.mint(owner.address, tokenId1)).to.be.revertedWithCustomError( + child, + 'ERC721TokenAlreadyMinted', + ); + }); + + it('cannot nest mint already minted token', async function () { + const parentId = 1; + const childId1 = 99; + await parent.mint(owner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted'); + }); + + it('cannot nest mint already minted token', async function () { + const parentId = 1; + const childId1 = 99; + await parent.mint(owner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.nestMint(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TokenAlreadyMinted'); + }); + + it('can mint with no destination', async function () { + const tokenId1 = 1; + await child.mint(tokenOwner.address, tokenId1); + expect(await child.ownerOf(tokenId1)).to.equal(tokenOwner.address); + expect(await child.directOwnerOf(tokenId1)).to.eql([tokenOwner.address, bn(0), false]); + }); + + it('has right owners', async function () { + const otherOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(tokenOwner.address, tokenId1); + const tokenId2 = 2; + await parent.mint(otherOwner.address, tokenId2); + const tokenId3 = 3; + await parent.mint(otherOwner.address, tokenId3); + + expect(await parent.ownerOf(tokenId1)).to.equal(tokenOwner.address); + expect(await parent.ownerOf(tokenId2)).to.equal(otherOwner.address); + expect(await parent.ownerOf(tokenId3)).to.equal(otherOwner.address); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await parent.balanceOf(otherOwner.address)).to.equal(2); + + await expect(parent.ownerOf(9999)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + }); + + it('cannot mint to zero address', async function () { + await expect(child.mint(ADDRESS_ZERO, 1)).to.be.revertedWithCustomError( + child, + 'ERC721MintToTheZeroAddress', + ); + }); + + it('cannot nest mint to a non-contract destination', async function () { + await expect(child.nestMint(tokenOwner.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'IsNotContract', + ); + }); + + it('cannot nest mint to non nestable receiver', async function () { + const ERC721 = await ethers.getContractFactory('ERC721Mock'); + const nonReceiver = await ERC721.deploy('Non receiver', 'NR'); + await nonReceiver.deployed(); + + await expect(child.nestMint(nonReceiver.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'MintToNonNestableImplementer', + ); + }); + + it('cannot nest mint to a non-existent token', async function () { + await expect(child.nestMint(parent.address, 1, 1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + }); + + it('cannot nest mint to zero address', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(child.nestMint(ADDRESS_ZERO, parentId, 1)).to.be.revertedWithCustomError( + child, + 'IsNotContract', + ); + }); + + it('can mint to contract and owners are ok', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // owner is the same adress + expect(await parent.ownerOf(parentId)).to.equal(tokenOwner.address); + expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + }); + + it('can mint to contract and direct owners are ok', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Direct owner is an address for the parent + expect(await parent.directOwnerOf(parentId)).to.eql([tokenOwner.address, bn(0), false]); + // Direct owner is a contract for the child + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + }); + + it("can mint to contract and parent's children are ok", async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + const children = await parent.childrenOf(parentId); + expect(children).to.eql([]); + + const pendingChildren = await parent.pendingChildrenOf(parentId); + expect(pendingChildren).to.eql([[bn(childId1), child.address]]); + expect(await parent.pendingChildOf(parentId, 0)).to.eql([bn(childId1), child.address]); + }); + + it('cannot get child out of index', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(parent.childOf(parentId, 0)).to.be.revertedWithCustomError( + parent, + 'ChildIndexOutOfRange', + ); + }); + + it('cannot get pending child out of index', async function () { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await expect(parent.pendingChildOf(parentId, 0)).to.be.revertedWithCustomError( + parent, + 'PendingChildIndexOutOfRange', + ); + }); + + it('can mint multiple children', async function () { + const parentId = 1; + const childId1 = 99; + const childId2 = 100; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(parent.address, childId2, parentId); + + expect(await child.ownerOf(childId1)).to.equal(tokenOwner.address); + expect(await child.ownerOf(childId2)).to.equal(tokenOwner.address); + + expect(await child.balanceOf(parent.address)).to.equal(2); + + const pendingChildren = await parent.pendingChildrenOf(parentId); + expect(pendingChildren).to.eql([ + [bn(childId1), child.address], + [bn(childId2), child.address], + ]); + }); + + it('can mint child into child', async function () { + const parentId = 1; + const childId1 = 99; + const granchildId = 999; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(child.address, granchildId, childId1); + + // Check balances -- yes, technically the counted balance indicates `child` owns an instance of itself + // and this is a little counterintuitive, but the root owner is the EOA. + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await child.balanceOf(child.address)).to.equal(1); + + const pendingChildrenOfChunky10 = await parent.pendingChildrenOf(parentId); + const pendingChildrenOfMonkey1 = await child.pendingChildrenOf(childId1); + + expect(pendingChildrenOfChunky10).to.eql([[bn(childId1), child.address]]); + expect(pendingChildrenOfMonkey1).to.eql([[bn(granchildId), child.address]]); + + expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]); + + expect(await child.ownerOf(granchildId)).to.eql(tokenOwner.address); + }); + + it('cannot have too many pending children', async () => { + const parentId = 1; + await parent.mint(tokenOwner.address, parentId); + + // First 128 should be fine. + for (let i = 1; i <= 128; i++) { + await child.nestMint(parent.address, i, parentId); + } + + await expect(child.nestMint(parent.address, 129, parentId)).to.be.revertedWithCustomError( + child, + 'MaxPendingChildrenReached', + ); + }); + }); + + describe('Interface support', async function () { + it('can support IERC165', async function () { + expect(await parent.supportsInterface('0x01ffc9a7')).to.equal(true); + }); + + it('can support IERC721', async function () { + expect(await parent.supportsInterface('0x80ac58cd')).to.equal(true); + }); + + it('can support INestable', async function () { + expect(await parent.supportsInterface('0x42b0e56f')).to.equal(true); + }); + + it('cannot support other interfaceId', async function () { + expect(await parent.supportsInterface('0xffffffff')).to.equal(false); + }); + }); + + describe('Adding child', async function () { + it('cannot add child from user address', async function () { + const tokenOwner1 = addrs[0]; + const tokenOwner2 = addrs[1]; + const parentId = 1; + await parent.mint(tokenOwner1.address, parentId); + const childId1 = 99; + await child.mint(tokenOwner2.address, childId1); + await expect(parent.addChild(parentId, childId1, '0x')).to.be.revertedWithCustomError( + parent, + 'IsNotContract', + ); + }); + }); + + describe('Accept child', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + }); + + it('can accept child', async function () { + await expect(parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1)) + .to.emit(parent, 'ChildAccepted') + .withArgs(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('can accept child if approved', async function () { + const approved = addrs[1]; + await parent.connect(tokenOwner).approve(approved.address, parentId); + await parent.connect(approved).acceptChild(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('can accept child if approved for all', async function () { + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator).acceptChild(parentId, 0, child.address, childId1); + await checkChildWasAccepted(); + }); + + it('cannot accept not owned child', async function () { + const notOwner = addrs[3]; + await expect( + parent.connect(notOwner).acceptChild(parentId, 0, child.address, childId1), + ).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot accept child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, otherChildId), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 0, otherAddress, childId1), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('cannot accept children for non existing index', async () => { + await expect( + parent.connect(tokenOwner).acceptChild(parentId, 1, child.address, childId1), + ).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange'); + }); + + async function checkChildWasAccepted() { + expect(await parent.pendingChildrenOf(parentId)).to.eql([]); + expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + } + }); + + describe('Rejecting children', async function () { + let parentId: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, 99, parentId); + }); + + it('can reject all pending children', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + await expect(parent.connect(tokenOwner).rejectAllChildren(parentId, 3)) + .to.emit(parent, 'AllChildrenRejected') + .withArgs(parentId); + await checkNoChildrenNorPending(parentId); + + // They are still on the child + expect(await child.balanceOf(parent.address)).to.equal(3); + }); + + it('cannot reject all pending children if there are more than expected', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + await expect( + parent.connect(tokenOwner).rejectAllChildren(parentId, 1), + ).to.be.revertedWithCustomError(parent, 'UnexpectedNumberOfChildren'); + }); + + it('can reject all pending children if approved', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + const rejecter = addrs[1]; + await parent.connect(tokenOwner).approve(rejecter.address, parentId); + await parent.connect(rejecter).rejectAllChildren(parentId, 3); + await checkNoChildrenNorPending(parentId); + }); + + it('can reject all pending children if approved for all', async function () { + // Mint a couple of more children + await child.nestMint(parent.address, 100, parentId); + await child.nestMint(parent.address, 101, parentId); + + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator).rejectAllChildren(parentId, 3); + await checkNoChildrenNorPending(parentId); + }); + + it('cannot reject all pending children for not owned pending child', async function () { + const notOwner = addrs[3]; + + await expect( + parent.connect(notOwner).rejectAllChildren(parentId, 2), + ).to.be.revertedWithCustomError(parent, 'ERC721NotApprovedOrOwner'); + }); + }); + + describe('Burning', async function () { + let parentId: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + }); + + it('can burn token', async function () { + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + await parent.connect(tokenOwner)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can burn token if approved', async function () { + const approved = addrs[1]; + await parent.connect(tokenOwner).approve(approved.address, parentId); + await parent.connect(approved)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can burn token if approved for all', async function () { + const operator = addrs[2]; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + await parent.connect(operator)['burn(uint256)'](parentId); + await checkBurntParent(); + }); + + it('can recursively burn nested token', async function () { + const childId1 = 99; + const granchildId = 999; + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(child.address, granchildId, childId1); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, granchildId); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await child.balanceOf(child.address)).to.equal(1); + + expect(await parent.childrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + expect(await child.childrenOf(childId1)).to.eql([[bn(granchildId), child.address]]); + expect(await child.directOwnerOf(granchildId)).to.eql([child.address, bn(childId1), true]); + + // Sets recursive burns to 2 + await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2); + + expect(await parent.balanceOf(tokenOwner.address)).to.equal(0); + expect(await child.balanceOf(parent.address)).to.equal(0); + expect(await child.balanceOf(child.address)).to.equal(0); + + await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + await expect(parent.directOwnerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + + await expect(child.ownerOf(childId1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + await expect(child.directOwnerOf(childId1)).to.be.revertedWithCustomError( + child, + 'ERC721InvalidTokenId', + ); + + await expect(parent.ownerOf(granchildId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + await expect(parent.directOwnerOf(granchildId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + }); + + it('can recursively burn nested token with the right number of recursive burns', async function () { + // Parent + // -> Child1 + // -> GrandChild1 + // -> GrandChild2 + // -> GreatGrandChild1 + // -> Child2 + // Total tree 5 (4 recursive burns) + const childId1 = 99; + const childId2 = 100; + const grandChild1 = 999; + const grandChild2 = 1000; + const greatGrandChild1 = 9999; + await child.nestMint(parent.address, childId1, parentId); + await child.nestMint(parent.address, childId2, parentId); + await child.nestMint(child.address, grandChild1, childId1); + await child.nestMint(child.address, grandChild2, childId1); + await child.nestMint(child.address, greatGrandChild1, grandChild2); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId2); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild1); + await child.connect(tokenOwner).acceptChild(childId1, 0, child.address, grandChild2); + await child.connect(tokenOwner).acceptChild(grandChild2, 0, child.address, greatGrandChild1); + + // 0 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 0)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, childId1); + // 1 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 1)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, grandChild1); + // 2 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 2)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, grandChild2); + // 3 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 3)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, greatGrandChild1); + // 4 is not enough + await expect(parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 4)) + .to.be.revertedWithCustomError(parent, 'MaxRecursiveBurnsReached') + .withArgs(child.address, childId2); + // 5 is just enough + await parent.connect(tokenOwner)['burn(uint256,uint256)'](parentId, 5); + }); + + async function checkBurntParent() { + expect(await parent.balanceOf(addrs[1].address)).to.equal(0); + await expect(parent.ownerOf(parentId)).to.be.revertedWithCustomError( + parent, + 'ERC721InvalidTokenId', + ); + } + }); + + describe('Transferring Active Children', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + childId1 = 99; + await parent.mint(tokenOwner.address, parentId); + await child.nestMint(parent.address, childId1, parentId); + await parent.connect(tokenOwner).acceptChild(parentId, 0, child.address, childId1); + }); + + it('can transfer child with to as root owner', async function () { + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, false, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + await checkChildMovedToRootOwner(); + }); + + it('can transfer child to another address', async function () { + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, false, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + await checkChildMovedToRootOwner(toOwnerAddress); + }); + + it('can transfer child to another NFT', async function () { + const newOwnerAddress = addrs[2].address; + const newParentId = 2; + await parent.mint(newOwnerAddress, newParentId); + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + parent.address, + newParentId, + 0, + child.address, + childId1, + false, + '0x', + ), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, false); + + expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]); + expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot transfer child out of index', async function () { + const toOwnerAddress = addrs[2].address; + const badIndex = 2; + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + toOwnerAddress, + 0, + badIndex, + child.address, + childId1, + false, + '0x', + ), + ).to.be.revertedWithCustomError(parent, 'ChildIndexOutOfRange'); + }); + + it('cannot transfer child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, false, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, false, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('can transfer child if approved', async function () { + const transferer = addrs[1]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).approve(transferer.address, parentId); + + await parent + .connect(transferer) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child if approved for all', async function () { + const operator = addrs[2]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + + await parent + .connect(operator) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child with grandchild and children are ok', async function () { + const toOwner = tokenOwner.address; + const grandchildId = 999; + await child.nestMint(child.address, grandchildId, childId1); + + // Transfer child from parent. + await parent + .connect(tokenOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]); + + // Grandchild is still owned by child + expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]); + }); + + it('cannot transfer child if not child root owner', async function () { + const toOwner = tokenOwner.address; + const notOwner = addrs[3]; + await expect( + parent + .connect(notOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, false, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot transfer child from not existing parent', async function () { + const badChildId = 99; + const toOwner = tokenOwner.address; + await expect( + parent + .connect(tokenOwner) + .transferChild(badChildId, toOwner, 0, 0, child.address, childId1, false, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId'); + }); + + async function checkChildMovedToRootOwner(rootOwnerAddress?: string) { + if (rootOwnerAddress === undefined) { + rootOwnerAddress = tokenOwner.address; + } + expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]); + + // Transferring updates balances downstream + expect(await child.balanceOf(rootOwnerAddress)).to.equal(1); + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + } + }); + + describe('Transferring Pending Children', async function () { + let parentId: number; + let childId1: number; + + beforeEach(async function () { + parentId = 1; + await parent.mint(tokenOwner.address, parentId); + childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + }); + + it('can transfer child with to as root owner', async function () { + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, tokenOwner.address, 0, 0, child.address, childId1, true, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + await checkChildMovedToRootOwner(); + }); + + it('can transfer child to another address', async function () { + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, childId1, true, '0x'), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + await checkChildMovedToRootOwner(toOwnerAddress); + }); + + it('can transfer child to another NFT', async function () { + const newOwnerAddress = addrs[2].address; + const newParentId = 2; + await parent.mint(newOwnerAddress, newParentId); + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + parent.address, + newParentId, + 0, + child.address, + childId1, + true, + '0x', + ), + ) + .to.emit(parent, 'ChildTransferred') + .withArgs(parentId, 0, child.address, childId1, true); + + expect(await child.ownerOf(childId1)).to.eql(newOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(newParentId), true]); + expect(await parent.pendingChildrenOf(newParentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot transfer child out of index', async function () { + const toOwnerAddress = addrs[2].address; + const badIndex = 2; + await expect( + parent + .connect(tokenOwner) + .transferChild( + parentId, + toOwnerAddress, + 0, + badIndex, + child.address, + childId1, + true, + '0x', + ), + ).to.be.revertedWithCustomError(parent, 'PendingChildIndexOutOfRange'); + }); + + it('cannot transfer child if address or id do not match', async function () { + const otherAddress = addrs[1].address; + const otherChildId = 9999; + const toOwnerAddress = addrs[2].address; + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, otherAddress, childId1, true, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + await expect( + parent + .connect(tokenOwner) + .transferChild(parentId, toOwnerAddress, 0, 0, child.address, otherChildId, true, '0x'), + ).to.be.revertedWithCustomError(parent, 'UnexpectedChildId'); + }); + + it('can transfer child if approved', async function () { + const transferer = addrs[1]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).approve(transferer.address, parentId); + + await parent + .connect(transferer) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child if approved for all', async function () { + const operator = addrs[2]; + const toOwner = tokenOwner.address; + await parent.connect(tokenOwner).setApprovalForAll(operator.address, true); + + await parent + .connect(operator) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'); + await checkChildMovedToRootOwner(); + }); + + it('can transfer child with grandchild and children are ok', async function () { + const toOwner = tokenOwner.address; + const grandchildId = 999; + await child.nestMint(child.address, grandchildId, childId1); + + // Transfer child from parent. + await parent + .connect(tokenOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([tokenOwner.address, bn(0), false]); + + // Grandchild is still owned by child + expect(await child.ownerOf(grandchildId)).to.eql(tokenOwner.address); + expect(await child.directOwnerOf(grandchildId)).to.eql([child.address, bn(childId1), true]); + }); + + it('cannot transfer child if not child root owner', async function () { + const toOwner = tokenOwner.address; + const notOwner = addrs[3]; + await expect( + parent + .connect(notOwner) + .transferChild(parentId, toOwner, 0, 0, child.address, childId1, true, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721NotApprovedOrOwner'); + }); + + it('cannot transfer child from not existing parent', async function () { + const badChildId = 99; + const toOwner = tokenOwner.address; + await expect( + parent + .connect(tokenOwner) + .transferChild(badChildId, toOwner, 0, 0, child.address, childId1, true, '0x'), + ).to.be.revertedWithCustomError(child, 'ERC721InvalidTokenId'); + }); + + async function checkChildMovedToRootOwner(rootOwnerAddress?: string) { + if (rootOwnerAddress === undefined) { + rootOwnerAddress = tokenOwner.address; + } + expect(await child.ownerOf(childId1)).to.eql(rootOwnerAddress); + expect(await child.directOwnerOf(childId1)).to.eql([rootOwnerAddress, bn(0), false]); + + // Transferring updates balances downstream + expect(await child.balanceOf(rootOwnerAddress)).to.equal(1); + expect(await parent.balanceOf(tokenOwner.address)).to.equal(1); + } + }); + + describe('Transfer', async function () { + it('can transfer token', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await parent.connect(firstOwner).transfer(newOwner.address, tokenId1); + + // Balances and ownership are updated + expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address); + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(newOwner.address)).to.equal(1); + }); + + it('cannot transfer not owned token', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await expect( + parent.connect(newOwner).transfer(newOwner.address, tokenId1), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot transfer to address zero', async function () { + const firstOwner = addrs[1]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + await expect( + parent.connect(firstOwner).transfer(ADDRESS_ZERO, tokenId1), + ).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress'); + }); + + it('can transfer token from approved address (not owner)', async function () { + const firstOwner = addrs[1]; + const approved = addrs[2]; + const newOwner = addrs[3]; + const tokenId1 = 1; + await parent.mint(firstOwner.address, tokenId1); + + await parent.connect(firstOwner).approve(approved.address, tokenId1); + await parent.connect(firstOwner).transfer(newOwner.address, tokenId1); + + expect(await parent.ownerOf(tokenId1)).to.eql(newOwner.address); + }); + + it('can transfer not nested token with child to address and owners/children are ok', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + await parent.connect(firstOwner).transfer(newOwner.address, parentId); + + // Balances and ownership are updated + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(newOwner.address)).to.equal(1); + + expect(await parent.ownerOf(parentId)).to.eql(newOwner.address); + expect(await parent.directOwnerOf(parentId)).to.eql([newOwner.address, bn(0), false]); + + // New owner of child + expect(await child.ownerOf(childId1)).to.eql(newOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + + // Parent still has its children + expect(await parent.pendingChildrenOf(parentId)).to.eql([[bn(childId1), child.address]]); + }); + + it('cannot directly transfer nested child', async function () { + const firstOwner = addrs[1]; + const newOwner = addrs[2]; + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + await expect( + child.connect(firstOwner).transfer(newOwner.address, childId1), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('can transfer parent token to token with same owner, family tree is ok', async function () { + const firstOwner = addrs[1]; + const grandParentId = 999; + await parent.mint(firstOwner.address, grandParentId); + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Check balances + expect(await parent.balanceOf(firstOwner.address)).to.equal(2); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // Transfers token parentId to (parent.address, token grandParentId) + await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId); + + // Balances unchanged since root owner is the same + expect(await parent.balanceOf(firstOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + expect(await parent.balanceOf(parent.address)).to.equal(1); + + // Parent is still owner of child + let expected = [bn(childId1), child.address]; + checkAcceptedAndPendingChildren(parent, parentId, [expected], []); + // Ownership: firstOwner > newGrandparent > parent > child + expected = [bn(parentId), parent.address]; + checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]); + }); + + it('can transfer parent token to token with different owner, family tree is ok', async function () { + const firstOwner = addrs[1]; + const otherOwner = addrs[2]; + const grandParentId = 999; + await parent.mint(otherOwner.address, grandParentId); + const parentId = 1; + await parent.mint(firstOwner.address, parentId); + const childId1 = 99; + await child.nestMint(parent.address, childId1, parentId); + + // Check balances + expect(await parent.balanceOf(otherOwner.address)).to.equal(1); + expect(await parent.balanceOf(firstOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // firstOwner calls parent to transfer parent token parent + await parent.connect(firstOwner).nestTransfer(parent.address, parentId, grandParentId); + + // Balances update + expect(await parent.balanceOf(firstOwner.address)).to.equal(0); + expect(await parent.balanceOf(parent.address)).to.equal(1); + expect(await parent.balanceOf(otherOwner.address)).to.equal(1); + expect(await child.balanceOf(parent.address)).to.equal(1); + + // Parent is still owner of child + let expected = [bn(childId1), child.address]; + checkAcceptedAndPendingChildren(parent, parentId, [expected], []); + // Ownership: firstOwner > newGrandparent > parent > child + expected = [bn(parentId), parent.address]; + checkAcceptedAndPendingChildren(parent, grandParentId, [], [expected]); + }); + }); + + describe('Nest Transfer', async function () { + let firstOwner: SignerWithAddress; + let parentId: number; + let childId1: number; + + beforeEach(async function () { + firstOwner = addrs[1]; + parentId = 1; + childId1 = 99; + await parent.mint(firstOwner.address, parentId); + await child.mint(firstOwner.address, childId1); + }); + + it('cannot nest tranfer from non immediate owner (owner of parent)', async function () { + const otherParentId = 2; + await parent.mint(firstOwner.address, otherParentId); + // We send it to the parent first + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + // We can no longer nest transfer it, even if we are the root owner: + await expect( + child.connect(firstOwner).nestTransfer(parent.address, childId1, otherParentId), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot nest tranfer to same NFT', async function () { + // We can no longer nest transfer it, even if we are the root owner: + await expect( + child.connect(firstOwner).nestTransfer(child.address, childId1, childId1), + ).to.be.revertedWithCustomError(child, 'NestableTransferToSelf'); + }); + + it('cannot nest tranfer a descendant same NFT', async function () { + // We can no longer nest transfer it, even if we are the root owner: + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + const grandChildId = 999; + await child.nestMint(child.address, grandChildId, childId1); + // Ownership is now parent->child->granChild + // Cannot send parent to grandChild + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, grandChildId), + ).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant'); + // Cannot send parent to child + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, childId1), + ).to.be.revertedWithCustomError(child, 'NestableTransferToDescendant'); + }); + + it('cannot nest tranfer if ancestors tree is too deep', async function () { + let lastId = childId1; + for (let i = 101; i <= 200; i++) { + await child.nestMint(child.address, i, lastId); + lastId = i; + } + // Ownership is now parent->child->child->child->child...->lastChild + // Cannot send parent to lastChild + await expect( + parent.connect(firstOwner).nestTransfer(child.address, parentId, lastId), + ).to.be.revertedWithCustomError(child, 'NestableTooDeep'); + }); + + it('cannot nest tranfer if not owner', async function () { + const notOwner = addrs[3]; + await expect( + child.connect(notOwner).nestTransfer(parent.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'NotApprovedOrDirectOwner'); + }); + + it('cannot nest tranfer to address 0', async function () { + await expect( + child.connect(firstOwner).nestTransfer(ADDRESS_ZERO, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'ERC721TransferToTheZeroAddress'); + }); + + it('cannot nest tranfer to a non contract', async function () { + const newOwner = addrs[2]; + await expect( + child.connect(firstOwner).nestTransfer(newOwner.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'IsNotContract'); + }); + + it('cannot nest tranfer to contract if it does implement INestable', async function () { + const ERC721 = await ethers.getContractFactory('ERC721Mock'); + const nonNestable = await ERC721.deploy('Non receiver', 'NR'); + await nonNestable.deployed(); + await expect( + child.connect(firstOwner).nestTransfer(nonNestable.address, childId1, parentId), + ).to.be.revertedWithCustomError(child, 'NestableTransferToNonNestableImplementer'); + }); + + it('can nest tranfer to INestable contract', async function () { + await child.connect(firstOwner).nestTransfer(parent.address, childId1, parentId); + expect(await child.ownerOf(childId1)).to.eql(firstOwner.address); + expect(await child.directOwnerOf(childId1)).to.eql([parent.address, bn(parentId), true]); + }); + + it('cannot nest tranfer to non existing parent token', async function () { + const notExistingParentId = 9999; + await expect( + child.connect(firstOwner).nestTransfer(parent.address, childId1, notExistingParentId), + ).to.be.revertedWithCustomError(parent, 'ERC721InvalidTokenId'); + }); + }); + + async function checkNoChildrenNorPending(parentId: number): Promise { + expect(await parent.pendingChildrenOf(parentId)).to.eql([]); + expect(await parent.childrenOf(parentId)).to.eql([]); + } + + async function checkAcceptedAndPendingChildren( + contract: EquippableTokenMock, + tokenId1: number, + expectedAccepted: any[], + expectedPending: any[], + ) { + const accepted = await contract.childrenOf(tokenId1); + expect(accepted).to.eql(expectedAccepted); + + const pending = await contract.pendingChildrenOf(tokenId1); + expect(pending).to.eql(expectedPending); + } +}); diff --git a/assets/eip-6220/test/renderUtils.ts b/assets/eip-6220/test/renderUtils.ts new file mode 100644 index 00000000000000..479139681d3c7b --- /dev/null +++ b/assets/eip-6220/test/renderUtils.ts @@ -0,0 +1,129 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { BigNumber } from 'ethers'; +import { ethers } from 'hardhat'; +import { EquippableTokenMock, EquipRenderUtils, MultiAssetRenderUtils } from '../typechain-types'; + +function bn(x: number): BigNumber { + return BigNumber.from(x); +} + +async function assetsFixture() { + const equipFactory = await ethers.getContractFactory('EquippableTokenMock'); + const renderUtilsFactory = await ethers.getContractFactory('MultiAssetRenderUtils'); + const renderUtilsEquipFactory = await ethers.getContractFactory('EquipRenderUtils'); + + const equip = await equipFactory.deploy(); + await equip.deployed(); + + const renderUtils = await renderUtilsFactory.deploy(); + await renderUtils.deployed(); + + const renderUtilsEquip = await renderUtilsEquipFactory.deploy(); + await renderUtilsEquip.deployed(); + + return { equip, renderUtils, renderUtilsEquip }; +} + +describe('Render Utils', async function () { + let owner: SignerWithAddress; + let someCatalog: SignerWithAddress; + let equip: EquippableTokenMock; + let renderUtils: MultiAssetRenderUtils; + let renderUtilsEquip: EquipRenderUtils; + let tokenId: number; + + const resId = bn(1); + const resId2 = bn(2); + const resId3 = bn(3); + const resId4 = bn(4); + + before(async function () { + ({ equip, renderUtils, renderUtilsEquip } = await loadFixture(assetsFixture)); + + const signers = await ethers.getSigners(); + owner = signers[0]; + someCatalog = signers[1]; + tokenId = 1; + + await equip.mint(owner.address, tokenId); + await equip.addEquippableAssetEntry( + resId, + 0, + ethers.constants.AddressZero, + 'ipfs://res1.jpg', + [], + ); + await equip.addEquippableAssetEntry(resId2, 1, someCatalog.address, 'ipfs://res2.jpg', [1, 3, 4]); + await equip.addEquippableAssetEntry( + resId3, + 0, + ethers.constants.AddressZero, + 'ipfs://res3.jpg', + [], + ); + await equip.addEquippableAssetEntry(resId4, 2, someCatalog.address, 'ipfs://res4.jpg', [4]); + await equip.addAssetToToken(tokenId, resId, 0); + await equip.addAssetToToken(tokenId, resId2, 0); + await equip.addAssetToToken(tokenId, resId3, resId); + await equip.addAssetToToken(tokenId, resId4, 0); + + await equip.acceptAsset(tokenId, 0, resId); + await equip.acceptAsset(tokenId, 1, resId2); + await equip.setPriority(tokenId, [10, 5]); + }); + + describe('Render Utils MultiAsset', async function () { + it('can get active assets', async function () { + expect(await renderUtils.getActiveAssets(equip.address, tokenId)).to.eql([ + [resId, 10, 'ipfs://res1.jpg'], + [resId2, 5, 'ipfs://res2.jpg'], + ]); + }); + + it('can get assets by id', async function () { + expect(await renderUtils.getAssetsById(equip.address, tokenId, [resId, resId2])).to.eql([ + 'ipfs://res1.jpg', + 'ipfs://res2.jpg', + ]); + }); + + it('can get pending assets', async function () { + expect(await renderUtils.getPendingAssets(equip.address, tokenId)).to.eql([ + [resId4, bn(0), bn(0), 'ipfs://res4.jpg'], + [resId3, bn(1), resId, 'ipfs://res3.jpg'], + ]); + }); + + it('can get top asset by priority', async function () { + expect(await renderUtils.getTopAssetMetaForToken(equip.address, tokenId)).to.eql( + 'ipfs://res2.jpg', + ); + }); + + it('cannot get top asset if token has no assets', async function () { + const otherTokenId = 2; + await equip.mint(owner.address, otherTokenId); + await expect( + renderUtils.getTopAssetMetaForToken(equip.address, otherTokenId), + ).to.be.revertedWithCustomError(renderUtils, 'TokenHasNoAssets'); + }); + }); + + describe('Render Utils Equip', async function () { + it('can get active assets', async function () { + expect(await renderUtilsEquip.getExtendedActiveAssets(equip.address, tokenId)).to.eql([ + [resId, bn(0), 10, ethers.constants.AddressZero, 'ipfs://res1.jpg', []], + [resId2, bn(1), 5, someCatalog.address, 'ipfs://res2.jpg', [bn(1), bn(3), bn(4)]], + ]); + }); + + it('can get pending assets', async function () { + expect(await renderUtilsEquip.getExtendedPendingAssets(equip.address, tokenId)).to.eql([ + [resId4, bn(2), bn(0), bn(0), someCatalog.address, 'ipfs://res4.jpg', [bn(4)]], + [resId3, bn(0), bn(1), resId, ethers.constants.AddressZero, 'ipfs://res3.jpg', []], + ]); + }); + }); +}); From 234eafd55cab78fbd03fb9baae3f9a5cba30012d Mon Sep 17 00:00:00 2001 From: Chris Whinfrey Date: Fri, 20 Jan 2023 15:01:39 -0800 Subject: [PATCH 189/274] Update EIP-5164: Add chainId and naming (#6146) * Add relayCall function * Fix typo * Add toChainId and fromChainId * Remove gasLimit * sender to from * Fix linter error * add author * relayer/executor to dispatcher/executor * Remove dispatch calls * nonce to messageId * Use message instead of call * Refactor MessageDispatcher with extensions * Fix indexes * Combine extension interfaces * PR feedback, typos, naming * Add messageId to MessageFailure error Co-authored-by: Pierrick Turelier * Add messageId to MessageBatchFailure error Co-authored-by: Pierrick Turelier * Fix typo Co-authored-by: Pierrick Turelier * Add getMessageExecutorAddress method Co-authored-by: Pierrick Turelier --- EIPS/eip-5164.md | 304 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 210 insertions(+), 94 deletions(-) diff --git a/EIPS/eip-5164.md b/EIPS/eip-5164.md index 357e12cafb1244..97f35699254053 100644 --- a/EIPS/eip-5164.md +++ b/EIPS/eip-5164.md @@ -2,7 +2,7 @@ eip: 5164 title: Cross-Chain Execution description: Defines an interface that supports execution across EVM networks. -author: Brendan Asselstine (@asselstine), Pierrick Turelier (@PierrickGT) +author: Brendan Asselstine (@asselstine), Pierrick Turelier (@PierrickGT), Chris Whinfrey (@cwhinfrey) discussions-to: https://ethereum-magicians.org/t/eip-5164-cross-chain-execution/9658 status: Review type: Standards Track @@ -12,9 +12,9 @@ created: 2022-06-14 ## Abstract -This specification defines a cross-chain execution interface for EVM-based blockchains. Implementations of this specification will allow contracts on one chain to call contracts on another. +This specification defines a cross-chain execution interface for EVM-based blockchains. Implementations of this specification will allow contracts on one chain to call contracts on another by sending a cross-chain message. -The specification defines two components: the "Cross Chain Relayer" and the "Cross Chain Executor". The Cross Chain Relayer lives on the calling side, and the executor lives on the receiving side. Calls sent to Cross Chain Relayers will move through a transport layer to Cross Chain Executor(s), where they are executed. Implementations of this specification must implement both components. +The specification defines two components: the "Message Dispatcher" and the "Message Executor". The Message Dispatcher lives on the calling side, and the executor lives on the receiving side. When a message is sent, a Message Dispatcher will move the message through a transport layer to a Message Executor, where they are executed. Implementations of this specification must implement both components. ## Motivation @@ -26,203 +26,319 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL This specification allows contracts on one chain to send messages to contracts on another chain. There are two key interfaces that needs to be implemented: -- `CrossChainRelayer` -- `CrossChainExecutor` +- `MessageDispatcher` +- `MessageExecutor` -The `CrossChainRelayer` lives on the origin chain. Users can share a single `CrossChainRelayer` or deploy their own. +The `MessageDispatcher` lives on the origin chain and dispatches messages to the `MessageExecutor` for execution. The `MessageExecutor` lives on the destination chain and executes dispatched messages. -The `CrossChainExecutor` lives on the destination chain and executes relayed calls. Users can share a single `CrossChainExecutor` or deploy their own. +There are also extensions of `MessageDispatcher`, each defining a method for initiating a message or message batch: -### CrossChainRelayer +- `SingleMessageDispatcher` +- `BatchMessageDispatcher` -The `CrossChainRelayer` lives on the chain from which messages are sent. The Relayer's job is to broadcast calls through a transport layer to one or more `CrossChainExecutor` contracts. +Alternatively, `MessageDispatcher`s may implement a custom interface for initiating messages. -#### Methods +### MessageDispatcher -**relayCalls** +The `MessageDispatcher` lives on the chain from which messages are sent. The Dispatcher's job is to broadcast messages through a transport layer to one or more `MessageExecutor` contracts. -Will relay a batch of `Call` structs to be executed by any `CrossChainExecutor`(s) that execute calls from this Relayer. The `gasLimit` is used as a limit on the executing side. +A unique `messageId` MUST be generated for each message or message batch. -`CrossChainRelayer`s MUST emit the `RelayedCalls` event when a batch of calls is relayed. +To ensure uniqueness, it is RECOMMENDED that a monotonically increasing nonce is used in the calculation of the `messageId`. -`CrossChainRelayer`s MUST increment a `nonce` so that each batch of calls can be uniquely identified. +#### MessageDispatcher Methods -`CrossChainRelayer`s MUST return the `nonce` to allow callers to track the batch of calls. +**getMessageExecutorAddress** -`CrossChainRelayer`s SHOULD pass the `nonce` as well as the address of the `sender` in the call to `CrossChainExecutor` to uniquely identify the message on the receiving chain. +Will return the address of `MessageExecutor` on the `toChainId`. -`CrossChainRelayer`s MAY require payment. +`MessageDispatcher`s MUST revert if the `toChainId` is not supported. ```solidity -struct Call { - address target; - bytes data; -} - -interface CrossChainRelayer { - function relayCalls(Call[] calldata calls, uint256 gasLimit) external payable returns (uint256 nonce); +interface MessageDispatcher { + function getMessageExecutorAddress(uint256 toChainId) external returns (address); } ``` ```yaml -- name: relayCalls +- name: getMessageExecutorAddress type: function - stateMutability: payable inputs: - - name: calls - type: Call[] - - name: gasLimit + - name: toChainId type: uint256 outputs: - - name: nonce - type: uint256 + - type: address ``` -#### Events +#### MessageDispatcher Events -**RelayedCalls** +**MessageDispatched** -The `RelayedCalls` event MUST be emitted by the `CrossChainRelayer` when `relayCalls` is called. +The `MessageDispatched` event MUST be emitted by the `MessageDispatcher` when an individual message is dispatched. ```solidity -interface CrossChainRelayer { - event RelayedCalls( - uint256 indexed nonce, - address indexed sender, - Call[] calls, - uint256 gasLimit +interface MessageDispatcher { + event MessageDispatched( + bytes32 indexed messageId, + address indexed from, + uint256 indexed toChainId, + address to, + bytes data, ); } ``` ```yaml -- name: RelayedCalls +- name: MessageDispatched type: event inputs: - - name: nonce + - name: messageId + indexed: true + type: bytes32 + - name: from + indexed: true + type: address + - name: toChainId indexed: true type: uint256 - - name: sender + - name: to + type: address + - name: data + type: bytes +``` + +**MessageBatchDispatched** + +The `MessageBatchDispatched` event MUST be emitted by the `MessageDispatcher` when a batch of messages is dispatched. + +```solidity +struct Message { + address to; + bytes data; +} + +interface MessageDispatcher { + event MessageBatchDispatched( + bytes32 indexed messageId, + address indexed from, + uint256 indexed toChainId, + Message[] messages + ); +} +``` + +```yaml +- name: MessageBatchDispatched + type: event + inputs: + - name: messageId + indexed: true + type: bytes32 + - name: from indexed: true type: address - - name: calls - type: Call[] - - name: gasLimit + - name: toChainId + indexed: true type: uint256 + - name: messages + type: Message[] ``` -#### Error handling +### SingleMessageDispatcher + +The `SingleMessageDispatcher` is an extension of `MessageDispatcher` that defines a method, `dispatchMessage`, for dispatching an individual message to be executed on the `toChainId`. + +#### SingleMessageDispatcher Methods + +**dispatchMessage** + +Will dispatch a message to be executed by the `MessageExecutor` on the destination chain specified by `toChainId`. -**GasLimitTooHigh** +`SingleMessageDispatcher`s MUST emit the `MessageDispatched` event when a message is dispatched. -`CrossChainRelayer`s MAY revert with `GasLimitTooHigh` if the `gasLimit` passed to `relayCalls` is higher than the maximum gas limit accepted by the bridge being used. +`SingleMessageDispatcher`s MUST revert if `toChainId` is not supported. + +`SingleMessageDispatcher`s MUST forward the message to a `MessageExecutor` on the `toChainId`. + +`SingleMessageDispatcher`s MUST use a unique `messageId` for each message. + +`SingleMessageDispatcher`s MUST return the `messageId` to allow the message sender to track the message. + +`SingleMessageDispatcher`s MAY require payment. ```solidity -interface CrossChainRelayer { - error GasLimitTooHigh( - uint256 gasLimit, - uint256 maxGasLimit - ); +interface SingleMessageDispatcher is MessageDispatcher { + function dispatchMessage(uint256 toChainId, address to, bytes calldata data) external payable returns (bytes32 messageId); +} +``` + +```yaml +- name: dispatchMessage + type: function + stateMutability: payable + inputs: + - name: toChainId + type: uint256 + - name: to + type: address + - name: data + type: bytes + outputs: + - name: messageId + type: bytes32 +``` + +### BatchedMessageDispatcher + +The `BatchedMessageDispatcher` is an extension of `MessageDispatcher` that defines a method, `dispatchMessageBatch`, for dispatching a batch of messages to be executed on the `toChainId`. + +#### BatchedMessageDispatcher Methods + +**dispatchMessageBatch** + +Will dispatch a batch of messages to be executed by the `MessageExecutor` on the destination chain specified by `toChainId`. + +`BatchedMessageDispatcher`s MUST emit the `MessageBatchDispatched` event when a message batch is dispatched. + +`BatchedMessageDispatcher`s MUST revert if `toChainId` is not supported. + +`BatchedMessageDispatcher`s MUST forward the message batch to the `MessageExecutor` on the `toChainId`. + +`BatchedMessageDispatcher`s MUST use a unique `messageId` for each batch of messages. + +`BatchedMessageDispatcher`s MUST return the `messageId` to allow the message sender to track the batch of messages. + +`BatchedMessageDispatcher`s MAY require payment. + +```solidity +interface BatchedMessageDispatcher is MessageDispatcher { + function dispatchMessageBatch(uint256 toChainId, Message[] calldata messages) external payable returns (bytes32 messageId); } ``` -### CrossChainExecutor +```yaml +- name: dispatchMessageBatch + type: function + stateMutability: payable + inputs: + - name: toChainId + type: uint256 + - name: messages + type: Message[] + outputs: + - name: messageId + type: bytes32 +``` + +### MessageExecutor -The `CrossChainExecutor` executes relayed calls. Developers must implement a `CrossChainExecutor` in order to execute messages on the receiving chain. +The `MessageExecutor` executes dispatched messages and message batches. Developers must implement a `MessageExecutor` in order to execute messages on the receiving chain. -The `CrossChainExecutor` will execute a nonce only once, but may execute nonces in any order. This specification makes no ordering guarantees, because messages may travel non-sequentially through the transport layer. +The `MessageExecutor` will execute a messageId only once, but may execute messageIds in any order. This specification makes no ordering guarantees, because messages and message batches may travel non-sequentially through the transport layer. #### Execution -`CrossChainExecutor`s SHOULD authenticate that the call has been performed by the bridge transport layer. +`MessageExecutor`s SHOULD verify all message data with the bridge transport layer. + +`MessageExecutor`s MUST NOT successfully execute a message more than once. -`CrossChainExecutor`s MUST NOT execute a batch of calls more than once. +`MessageExecutor`s MUST revert the transaction when a message fails to be executed allowing the message to be retried at a later time. **Calldata** -`CrossChainExecutor`s MUST append the ABI-packed (`nonce`, `sender`) to the calldata for each call being executed. It allows the receiver of the call to check the true `sender` of the transaction and use the `nonce` to apply any transaction ordering logic. +`MessageExecutor`s MUST append the ABI-packed (`messageId`, `fromChainId`, `from`) to the calldata for each message being executed. This allows the receiver of the message to verify the cross-chain sender and the chain that the message is coming from. ```solidity -interface CrossChainExecutor { - bytes calldata = abi.encode(Call.data, nonce, sender); // Can also use abi.encodePacked -} +to.call(abi.encodePacked(data, messageId, fromChainId, from)); ``` ```yaml - name: calldata type: bytes inputs: - - name: Call.data + - name: data type: bytes - - name: nonce + - name: messageId + type: bytes32 + - name: fromChainId type: uint256 - - name: sender + - name: from type: address ``` #### Error handling -**CallsAlreadyExecuted** +**MessageAlreadyExecuted** -`CrossChainExecutor`s MUST revert if a batch of calls has already been executed and SHOULD emit `CallsAlreadyExecuted` custom error. +`MessageExecutor`s MUST revert if a messageId has already been executed and SHOULD emit a `MessageIdAlreadyExecuted` custom error. ```solidity -interface CrossChainExecutor { - error CallsAlreadyExecuted( - uint256 nonce +interface MessageExecutor { + error MessageIdAlreadyExecuted( + bytes32 messageId ); } ``` -**CallFailure** +**MessageFailure** -`CrossChainExecutor`s SHOULD revert with a `CallFailure` error if a call fails. +`MessageExecutor`s MUST revert if an individual message fails and SHOULD emit a `MessageFailure` custom error. ```solidity -interface CrossChainExecutor { - error CallFailure( - uint256 callIndex, +interface MessageExecutor { + error MessageFailure( + bytes32 messageId, bytes errorData ); } ``` -#### Events +**MessageBatchFailure** -**ExecutedCalls** +`MessageExecutor`s MUST revert the entire batch if any message in a batch fails and SHOULD emit a `MessageBatchFailure` custom error. -`ExecutedCalls` MUST be emitted once calls have been executed. +```solidity +interface MessageExecutor { + error MessageBatchFailure( + bytes32 messageId, + uint256 messageIndex, + bytes errorData + ); +} +``` + +#### MessageExecutor Events + +**MessageIdExecuted** + +`MessageIdExecuted` MUST be emitted once a message or message batch has been executed. ```solidity -interface CrossChainExecutor { - event ExecutedCalls( - CrossChainRelayer indexed relayer, - uint256 indexed nonce +interface MessageExecutor { + event MessageIdExecuted( + uint256 indexed fromChainId, + bytes32 indexed messageId ); } ``` ```yaml -- name: ExecutedCalls +- name: MessageIdExecuted type: event inputs: - - name: relayer - indexed: true - type: CrossChainRelayer - - name: nonce + - name: fromChainId indexed: true type: uint256 + - name: messageId + indexed: true + type: bytes32 ``` ## Rationale -The `CrossChainRelayer` can be coupled to one or more `CrossChainExecutor`. It is up to bridges to decide how to couple the two. Users can easily bridge a message by calling `relayCalls` without being aware of the `CrossChainExecutor` address. Messages can also be traced by a client using the data logged by the `ExecutedCalls` event. - -Calls are relayed in batches because it is such a common action. Rather than have implementors take different approaches to encoding multiple calls into the `data` portion, this specification includes call batching to take away any guess work. - -Some bridges may require payment in the native currency, so the `relayCalls` function is payable. +The `MessageDispatcher` can be coupled to one or more `MessageExecutor`. It is up to bridges to decide how to couple the two. Users can easily bridge a message by calling `dispatchMessage` without being aware of the `MessageExecutor` address. Messages can also be traced by a client using the data logged by the `MessageIdExecuted` event. -Bridges relay messages in various ways, applications should be aware of how the bridge they rely on operates and decide if they want to enforce transaction ordering by using the `nonce`. +Some bridges may require payment in the native currency, so the `dispatchMessage` function is payable. ## Backwards Compatibility From c7c44aaa7baf401328e6c0e5a9ee1de49c31d97a Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sat, 21 Jan 2023 10:06:36 -0500 Subject: [PATCH 190/274] Update EIP-4834: Move to Final (#6363) --- EIPS/eip-4834.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-4834.md b/EIPS/eip-4834.md index 9310db9baf2c0e..8ddcc36d958b7e 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: 2023-01-20 +status: Final type: Standards Track category: ERC created: 2022-02-22 From 3927907a647cc63b3ae42f479cd8355261d3a0a5 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Sat, 21 Jan 2023 10:06:55 -0500 Subject: [PATCH 191/274] Update EIP-2771: Move to Final (#6362) --- EIPS/eip-2771.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-2771.md b/EIPS/eip-2771.md index b2d0542e1d97b2..9fb1ba84e03cf8 100644 --- a/EIPS/eip-2771.md +++ b/EIPS/eip-2771.md @@ -4,8 +4,7 @@ title: Secure Protocol for Native Meta Transactions 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: Last Call -last-call-deadline: 2023-01-20 +status: Final type: Standards Track category: ERC created: 2020-07-01 From 1b4112a8207ffe0e54910c772e6d12d2886300d8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 22 Jan 2023 23:02:05 -0700 Subject: [PATCH 192/274] fix RJUMPI -> JUMPI (#6368) --- EIPS/eip-4200.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-4200.md b/EIPS/eip-4200.md index fc744bc6636c39..19497a60abcc80 100644 --- a/EIPS/eip-4200.md +++ b/EIPS/eip-4200.md @@ -77,7 +77,7 @@ Should there be a need to have immediate encodings of other size (such as 8-bits ### `PUSHn JUMP` sequences -If we chose absolute addressing, then `RJUMP` could be viewed similar to the sequence `PUSHn JUMP` (and `RJUMPI` similar to `PUSHn RJUMPI`). In that case one could argue that instead of introducing a new instruction, such sequences should get a discount, because EVMs could optimise them. +If we chose absolute addressing, then `RJUMP` could be viewed similar to the sequence `PUSHn JUMP` (and `RJUMPI` similar to `PUSHn JUMPI`). In that case one could argue that instead of introducing a new instruction, such sequences should get a discount, because EVMs could optimise them. We think this is a bad direction to go: From 394bdcca14c2417b086e03dff489e30b67436e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 23 Jan 2023 10:52:17 +0100 Subject: [PATCH 193/274] EIP-3540: Add @lightclient as author (#6347) --- EIPS/eip-3540.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-3540.md b/EIPS/eip-3540.md index b9a3fcfc31f490..78f594f49303be 100644 --- a/EIPS/eip-3540.md +++ b/EIPS/eip-3540.md @@ -2,7 +2,7 @@ eip: 3540 title: EOF - EVM Object Format v1 description: EOF is an extensible and versioned container format for EVM bytecode with a once-off validation at deploy time. -author: Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0) +author: Alex Beregszaszi (@axic), Paweł Bylica (@chfast), Andrei Maiboroda (@gumb0), Matt Garnett (@lightclient) discussions-to: https://ethereum-magicians.org/t/evm-object-format-eof/5727 status: Review type: Standards Track From 0f3191f6e8a1613972d7fdaecc01470c50bd00b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 23 Jan 2023 10:54:46 +0100 Subject: [PATCH 194/274] EIP-5450: Extend motivation of AOT/JIT compilation (#6346) * EIP-5450: Extend motivation of AOT/JIT compilation * Apply suggestions from code review Co-authored-by: Andrei Maiboroda Co-authored-by: Danno Ferrin Co-authored-by: Andrei Maiboroda --- EIPS/eip-5450.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-5450.md b/EIPS/eip-5450.md index 1a7fb49a9448f6..d419ce19f625cb 100644 --- a/EIPS/eip-5450.md +++ b/EIPS/eip-5450.md @@ -2,7 +2,7 @@ eip: 5450 title: EOF - Stack Validation description: Deploy-time validation of stack usage for EOF functions. -author: Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast), Alex Beregszaszi (@axic) +author: Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast), Alex Beregszaszi (@axic), Danno Ferrin (@shemnon) discussions-to: https://ethereum-magicians.org/t/eip-5450-eof-stack-validation/10410 status: Review type: Standards Track @@ -36,6 +36,8 @@ It also has some disadvantages: - adds constraints to the code structure (similar to JVM, CPython bytecode, WebAssembly and others); however, these constraints can be lifted in a backward-compatible manner if they are shown to be user-unfriendly, - adds second validation pass; however, validation's computational and space complexity remains linear. +The guarantees created by these validation rules also improve the feasabiliy of Ahead-Of-Time and Just-In-Time compilation of EVM code. Single pass transpilation passes can be safely executed with the code validation and advanced stack/register handling can be applied with the stack height validaitons. While not as impactful to a mainnet validator node that is bound mostly by storage state sizes, these can significantly speed up witness validation and other non-mainnet use cases. + ## Specification ### Code validation From ce37c2ae7a9746472e3cf996e48402bfa2a7ae4a Mon Sep 17 00:00:00 2001 From: xinbenlv Date: Tue, 24 Jan 2023 06:11:53 -0800 Subject: [PATCH 195/274] Move EIP-5298 to Review (#5934) From c2f9703c35f953e5ae274b01146a300bbf8262fc Mon Sep 17 00:00:00 2001 From: ashhanai Date: Tue, 24 Jan 2023 14:12:33 +0000 Subject: [PATCH 196/274] Update EIP-5646: Move to Final (#6365) * Add EIP-5646: Token state fingerprint * Update EIP-5646: add link to discussion * Update EIP-5646: reference ERC standards as EIP-N * Update EIP-5646: update all EIP references to links * Update EIP-5646: fix incorrect EIP references * Update EIP-5646: Rework the structure of the whole draft * Update EIP-5646: Move to Final --- EIPS/eip-5646.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/EIPS/eip-5646.md b/EIPS/eip-5646.md index d7696e0424d204..db381c1f58fac1 100644 --- a/EIPS/eip-5646.md +++ b/EIPS/eip-5646.md @@ -4,8 +4,7 @@ title: Token State Fingerprint description: Unambiguous token state identifier author: Naim Ashhab (@ashhanai) discussions-to: https://ethereum-magicians.org/t/eip-5646-discussion-token-state-fingerprint/10808 -status: Last Call -last-call-deadline: 2022-12-21 +status: Final type: Standards Track category: ERC created: 2022-09-11 From 8d864ed31396543751e82929ff637955b9d655a1 Mon Sep 17 00:00:00 2001 From: Dhruv Malik Date: Tue, 24 Jan 2023 15:14:11 +0100 Subject: [PATCH 197/274] Update EIP-3475: Changing comments (#5789) * some minor description changes in interface and example implemenation. * rectifying description of the array struct. * Update assets/eip-3475/interfaces/IERC3475.sol grammer refactor Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update assets/eip-3475/interfaces/IERC3475.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update assets/eip-3475/interfaces/IERC3475.sol Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * Update assets/eip-3475/interfaces/IERC3475.sol refactor Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * refactoring and addressing ambiguous descriptions * refactor * refactor final * adding correct syntax and all refactoring done !! * spaces and refactoring * update Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> * adding the comments in the interfaces from the standard description. * separating the examples for events, decluttering. * updating the example class name * removing redundant metadata description. * refactor the standard based on feedback PR #6128 * add example * modified: EIPS/eip-3475.md - Rectifying the example on explaining the approval function(L#126). - Changing the formatting of subheadings in rational to be better visiblity and linting. (L#268). - Rectifying the definition of `event Redeem()` to include transaction struct parameter. (L#297). - Linting suggestion(L#346,L#385, L#404). - Re-writing the sentence that defines the rational of having metadata structure(L#365) modified: assets/eip-3475/ERC3475.sol - clarifying the correct definition of value in metadata. Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> --- EIPS/eip-3475.md | 120 ++++++++++++++---------- assets/eip-3475/ERC3475.sol | 23 +++-- assets/eip-3475/interfaces/IERC3475.sol | 19 +++- 3 files changed, 100 insertions(+), 62 deletions(-) diff --git a/EIPS/eip-3475.md b/EIPS/eip-3475.md index c8b909f94c8712..f55b6dc3e3334d 100644 --- a/EIPS/eip-3475.md +++ b/EIPS/eip-3475.md @@ -13,19 +13,20 @@ requires: 20, 721, 1155 ## Abstract -- This EIP allows the creation of tokenized obligations with abstract on-chain metadata storage. Issuing bonds with multiple redemption data cannot be achieved with existing token standards. +- This EIP allows the creation of tokenized obligations with abstract on-chain metadata storage. Issuing bonds with multiple redemption data cannot be achieved with existing token standards. - This EIP enables each bond class ID to represent a new configurable token type and corresponding to each class, corresponding bond nonces to represent an issuing condition or any other form of data in uint256. Every single nonce of a bond class can have its metadata, supply, and other redemption conditions. - Bonds created by this EIP can also be batched for issuance/redemption conditions for efficiency on gas costs and UX side. And finally, bonds created from this standard can be divided and exchanged in a secondary market. ## Motivation + Current LP (Liquidity Provider) tokens are simple [EIP-20](./eip-20.md) tokens with no complex data structure. To allow more complex reward and redemption logic to be stored on-chain, we need a new token standard that: - - Supports multiple token IDs - - Can store on-chain metadata - - Doesn't require a fixed storage pattern - - Is gas-efficient. +- Supports multiple token IDs +- Can store on-chain metadata +- Doesn't require a fixed storage pattern +- Is gas-efficient. Also Some benefits: @@ -50,9 +51,11 @@ pragma solidity ^0.8.0; * @param _to argument is the address of the bond recipient whose balance is about to increase. * @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). +e.g: +* 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(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; /** @@ -62,10 +65,12 @@ function transferFrom(address _from, address _to, Transaction[] calldata _transa * @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] ) +e.g: +* 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(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 + +function transferAllowanceFrom(address _from,address _to, Transaction[] calldata _transactions) public ; /** * issue @@ -74,9 +79,10 @@ function transferAllowanceFrom(address _from,address _to, Transaction[] calldata * @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] ) +e.g: +example: issue(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); +issues `1000` bonds with a class of `0` to address `0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef` with a nonce of `5`. */ -// 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; /** @@ -87,9 +93,11 @@ function issue(address _to, Transaction[] calldata _transaction) external; * @param `_transactions` is the `Transaction[] calldata` structure (i.e., array of tuples with the pairs of (class, nonce and amount) of the bonds that are to be redeemed). Further defined in the rationale section. * @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. +e.g: +* redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, [IERC3475.Transaction(1,14,500)]); +means “redeem from wallet address(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef), 500000000 of bond class1 and nonce 42. */ -// 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; /** @@ -100,9 +108,10 @@ function redeem(address _from, Transaction[] calldata _transactions) external; * @param `_transactions` is the `Transaction[] calldata` structure (i.e., array of tuple with the pairs of (class, nonce and amount) of the bonds that are to be burned). further defined in the rationale. * @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. +* e.g: +* burn(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]); +* means burning 500000000 bonds of class 1 nonce 42 owned by address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B. */ -// 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; /** @@ -112,9 +121,11 @@ function burn(address _from, Transaction[] calldata _transactions) external; * @dev `approve()` should only be callable by the bank, or the owner of the account. * @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. +* e.g: +* approve(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,[IERC3475.Transaction(1,14,500)]); +* means owner of address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is approved to manage 500 bonds from class 1 and Nonce 14. */ -// 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; /** @@ -126,9 +137,10 @@ function approve(address _spender, Transaction[] calldata _transactions) externa * @param `classId` is the class id of the bond. * @param `_approved` is true if the operator is approved (based on the conditions provided), false meaning approval is revoked. * @dev contract MUST define internal function regarding the conditions for setting approval and should be callable only by bank or owner. +* e.g: setApprovalFor(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,0,true); +* means that address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is authorized to transfer bonds from class 0 (across all nonces). */ -// setApprovalFor(0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B,0,true); -// means that address 0x82a55a613429Aeb3D01fbE6841bE1AcA4fFD5b2B is authorized to transfer bonds from class 0 (across all nonces). + function setApprovalFor(address _operator, bool _approved) external returns(bool approved); /** @@ -137,9 +149,10 @@ function setApprovalFor(address _operator, bool _approved) external returns(bool * @param classId is the corresponding class Id of the bond. * @param nonceId is the nonce Id of the given bond class. * @return the supply of the bonds +* e.g: +* totalSupply(0, 1); +* it finds the total supply of the bonds of classid 0 and bond nonce 1. */ -// totalSupply(0, 1); -// it finds the total supply of the bonds of classid 0 and bond nonce 1. function totalSupply(uint256 classId, uint256 nonceId) external view returns (uint256); /** @@ -239,7 +252,7 @@ function getProgress(uint256 classId, uint256 nonceId) external view returns (ui * @param nonceId is the nonceId of the given bond class. * @notice Returns the _amount which spender is still allowed to withdraw from _owner. */ -function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external view returns(uint256); +function allowance(address _owner, address _spender, uint256 classId, uint256 nonceId) external returns(uint256); /** * isApprovedFor @@ -252,7 +265,7 @@ function allowance(address _owner, address _spender, uint256 classId, uint256 no function isApprovedFor(address _owner, address _operator) external view returns (bool); ``` -**Events** +### Events ```solidity /** @@ -260,47 +273,51 @@ function isApprovedFor(address _owner, address _operator) external view returns * @notice Issue MUST trigger when Bonds are issued. This SHOULD not include zero value Issuing. * @dev This SHOULD not include zero value issuing. * @dev Issue MUST be triggered when the operator (i.e Bank address) contract issues bonds to the given entity. +* eg: emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); +* issue by address(operator) 500 Bonds(nonce14,class 1) to address 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. */ + event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions); -// eg: - -emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); -// issue by address(operator) 500 DBIT-USD Bond(nonce14,class 0) to address 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. /** * Redeem * @notice Redeem MUST trigger when Bonds are redeemed. This SHOULD not include zero value redemption. +*e.g: 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. */ -event Redeem(address indexed _operator, address indexed _from, uint256 classId, uint256 nonceId, uint256 _amount); -//e.g: -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. + +event Redeem(address indexed _operator, address indexed _from, Transaction[] _transactions); + /** * Burn. * @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. +* e.g : emit Burn(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]); +* emits event when 500 bonds of owner 0x492Af743654549b12b1B807a9E0e8F397E44236E of type (class 1, nonce 14) are burned by operator 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. */ - 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. + +event burn(address _operator, address _owner, Transaction[] _transactions); /** * Transfer * @dev its emitted when the bond is transferred by address(operator) from owner address(_from) to address(_to) with the bonds transferred, whose params are defined by _transactions struct array. * @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). +* e.g emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, _to, [IERC3475.Transaction(1,14,500)]); +* transfer by address(_operator) amount 500 bonds with (Class 1 and Nonce 14) from 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, to address(_to). */ -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); /** * ApprovalFor * @dev its emitted when address(_owner) approves the address(_operator) to transfer his bonds. * @notice Approval MUST trigger when bond holders are approving an _operator. This SHOULD not include zero value approval. +* eg: emit ApprovalFor(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E, true); +* this means 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef gives 0x492Af743654549b12b1B807a9E0e8F397E44236E access permission for transfer of its bonds. */ -emit ApprovalFor(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E); -// this means 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef gives 0x492Af743654549b12b1B807a9E0e8F397E44236E access permission for transfer of its bonds. + event ApprovalFor(address indexed _owner, address indexed _operator, bool _approved); ``` @@ -326,17 +343,17 @@ Some specific examples of metadata can be the localization of bonds, jurisdictio This structure defines the details of the class information (symbol, risk information, etc.). the example is explained [here](../assets/eip-3475/Metadata.md) in the class metadata section. -### 4. Decoding data: +### 4. Decoding data -First, the functions for analyzing the metadata (i.e `ClassMetadata` and `NonceMetadata`) are to be used by the corresponding frontend to decode the information of the bond. +First, the functions for analyzing the metadata (i.e `ClassMetadata` and `NonceMetadata`) are to be used by the corresponding frontend to decode the information of the bond. -This is done via overriding the function interface for functions `classValues` and `nonceValues` by defining the key (which SHOULD be an index) to read the corresponding information stored as a JSON object. +This is done via overriding the function interface for functions `classValues` and `nonceValues` by defining the key (which SHOULD be an index) to read the corresponding information stored as a JSON object. ```JSON { "title": "symbol", "_type": "string", -"description": "Lorem ipsum..." +"description": "defines the unique identifier name in following format: (symbol, bondType, maturity in months)", "values": ["Class Name 1","Class Name 2","DBIT Fix 6M"], } ``` @@ -345,7 +362,9 @@ e.g. In the above example, to get the `symbol` of the given class id, we can use ## Rationale -**Metadata structure**: Instead of utilizing a mapping from address, the bond's metadata like the time of redemption, redemption conditions, and interest rate can be stored in the bond class and nonce structures. Classes represent the different bond types, and nonces represent the various period of issuances. Nonces under the same class share the same metadata. Meanwhile, nonces are non-fungible. Each nonce can store a different set of metadata. Thus, upon transfer of a bond, all the metadata will be transferred to the new owner of the bond. +### Metadata structure + +Instead of storing the details about the class and their issuances to the user (ie nonce) externally, we store the details in the respective structures. Classes represent the different bond types, and nonces represent the various period of issuances. Nonces under the same class share the same metadata. Meanwhile, nonces are non-fungible. Each nonce can store a different set of metadata. Thus, upon transfer of a bond, all the metadata will be transferred to the new owner of the bond. ```solidity struct Values{ @@ -353,6 +372,7 @@ e.g. In the above example, to get the `symbol` of the given class id, we can use uint uintValue; address addressValue; bool boolValue; + bytes bytesValue; } ``` @@ -364,8 +384,7 @@ e.g. In the above example, to get the `symbol` of the given class id, we can use } ``` - -**Batch function:** +### Batch function This EIP supports batch operations. It allows the user to transfer different bonds along with their metadata to a new address instantaneously in a single transaction. After execution, the new owner holds the right to reclaim the face value of each of the bonds. This mechanism helps with the "packaging" of bonds–helpful in use cases like trades on a secondary market. @@ -384,8 +403,9 @@ The `nonceId` is the nonce id of the given bond class. This param is for distinc The `_amount` is the amount of the bond for which the spender is approved. +### AMM optimization -**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. + 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. @@ -401,7 +421,7 @@ To ensure the indexing of transactions throughout the bond lifecycle (i.e "Issue 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 fungible (like [EIP-20](./eip-20.md)), non-fungible ([EIP-721](./eip-721.md), [EIP-1155](./eip-1155.md)) , or other bonds represented by this standard. ## Test Cases @@ -412,16 +432,14 @@ 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 -- The `function setApprovalFor()` gives the operator role in this standard. It has all the permissions to transfer, burn and redeem bonds by default. +- The `function setApprovalFor(address _operatorAddress)` gives the operator role to `_operatorAddress`. It has all the permissions to transfer, burn and redeem bonds by default. - 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. - ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/eip-3475/ERC3475.sol b/assets/eip-3475/ERC3475.sol index 157476587c42f1..43448e132e1999 100644 --- a/assets/eip-3475/ERC3475.sol +++ b/assets/eip-3475/ERC3475.sol @@ -49,16 +49,18 @@ contract ERC3475 is IERC3475 { _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"; - _classes[1]._values[0].stringValue = "DBIT Fix test Instantaneous"; - // define "period of the class"; _classMetadata[5].title = "period"; _classMetadata[5]._type = "int"; - _classMetadata[5].description = "details about issuance and redemption time"; + _classMetadata[5].description = "value (in months) about maturity time"; + + + + + // describing the symbol of the different class + _classes[0]._values[0].stringValue = "DBIT Fix 6M"; + _classes[1]._values[0].stringValue = "DBIT Fix test Instantaneous"; + // define the maturity time period (for the test class). _classes[0]._values[5].uintValue = 10; @@ -74,15 +76,16 @@ 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 metadata explaining "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"; + _classes[1]._nonceMetadata[0].title = "maturity"; - _classes[0]._nonceMetadata[0]._type = "int"; + _classes[1]._nonceMetadata[0]._type = "int"; _classes[1]._nonceMetadata[0].description = "maturity date in integer"; - // defining the value status + // initializing all of the nonces for issued bonds _classes[0].nonces[0]._values[0].boolValue = true; _classes[0].nonces[1]._values[0].boolValue = true; _classes[0].nonces[2]._values[0].boolValue = true; diff --git a/assets/eip-3475/interfaces/IERC3475.sol b/assets/eip-3475/interfaces/IERC3475.sol index 9dc5a45e06deb4..9fa2b1b44c2c5e 100644 --- a/assets/eip-3475/interfaces/IERC3475.sol +++ b/assets/eip-3475/interfaces/IERC3475.sol @@ -180,22 +180,39 @@ interface IERC3475 { // EVENTS /** * @notice MUST trigger when tokens are transferred, including zero value transfers. + * e.g: + emit Transfer(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef, 0x492Af743654549b12b1B807a9E0e8F397E44236E,0x3d03B6C79B75eE7aB35298878D05fe36DC1fEf, [IERC3475.Transaction(1,14,500)]) + means that operator 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef wants to transfer 500 bonds of class 1 , Nonce 14 of owner 0x492Af743654549b12b1B807a9E0e8F397E44236E to address 0x3d03B6C79B75eE7aB35298878D05fe36DC1fEf. */ event Transfer(address indexed _operator, address indexed _from, address indexed _to, Transaction[] _transactions); /** * @notice MUST trigger when tokens are issued + * @notice Issue MUST trigger when Bonds are issued. This SHOULD not include zero value Issuing. + * @dev This SHOULD not include zero value issuing. + * @dev Issue MUST be triggered when the operator (i.e Bank address) contract issues bonds to the given entity. + eg: emit Issue(_operator, 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,[IERC3475.Transaction(1,14,500)]); + issue by address(operator) 500 Bonds(nonce14,class 0) to address 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. */ event Issue(address indexed _operator, address indexed _to, Transaction[] _transactions); /** - * @notice MUST trigger when tokens are redeemed + * @notice MUST trigger when tokens are redeemed. + * @notice Redeem MUST trigger when Bonds are redeemed. This SHOULD not include zero value redemption. + * eg: emit Redeem(0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef,0x492Af743654549b12b1B807a9E0e8F397E44236E,[IERC3475.Transaction(1,14,500)]); + * this emit event when 5000 bonds of class 1, nonce 14 owned by address 0x492Af743654549b12b1B807a9E0e8F397E44236E are being redeemed by 0x2d03B6C79B75eE7aB35298878D05fe36DC1fE8Ef. */ event Redeem(address indexed _operator, address indexed _from, Transaction[] _transactions); /** * @notice MUST trigger when tokens are burned + * @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 + * @notice 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. */ event Burn(address indexed _operator, address indexed _from, Transaction[] _transactions); /** * @dev MUST emit when approval for a second party/operator address to manage all bonds from a classId given for an owner address is enabled or disabled (absence of an event assumes disabled). + * @dev its emitted when address(_owner) approves the address(_operator) to transfer his bonds. + * @notice Approval MUST trigger when bond holders are approving an _operator. This SHOULD not include zero value approval. */ event ApprovalFor(address indexed _owner, address indexed _operator, bool _approved); } From 66e3399fd708a4d2438cf800fdb05c57ab6c6674 Mon Sep 17 00:00:00 2001 From: Suning Yao Date: Tue, 24 Jan 2023 09:30:53 -0500 Subject: [PATCH 198/274] Update EIP-6150: Move to Last Call (#6311) --- EIPS/eip-6150.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-6150.md b/EIPS/eip-6150.md index 952a36f92c6754..f88da482e745ac 100644 --- a/EIPS/eip-6150.md +++ b/EIPS/eip-6150.md @@ -4,7 +4,8 @@ title: Hierarchical NFTs description: Hierarchical NFTs, an extension to EIP-721. author: Keegan Lee (@keeganlee), msfew , Kartin , qizhou (@qizhou) discussions-to: https://ethereum-magicians.org/t/eip-6150-hierarchical-nfts-an-extension-to-erc-721/12173 -status: Review +status: Last Call +last-call-deadline: 2023-01-25 type: Standards Track category: ERC created: 2022-12-15 From f26568d954d666af60930563d8469855b62a3f28 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 25 Jan 2023 15:17:39 +0100 Subject: [PATCH 199/274] Add EIP-6372: Contract clock (#6372) * add new contract clock EIP * numbring * add discussion link * spelling * add missing section --- EIPS/eip-6372.md | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 EIPS/eip-6372.md diff --git a/EIPS/eip-6372.md b/EIPS/eip-6372.md new file mode 100644 index 00000000000000..45fc0c266d4a02 --- /dev/null +++ b/EIPS/eip-6372.md @@ -0,0 +1,92 @@ +--- +eip: 6372 +title: Contract clock +description: An interface for exposing a contract's clock value and details +author: Hadrien Croubois (@Amxx), Francisco Giordano (@frangio) +discussions-to: https://ethereum-magicians.org/t/eip-6372-contract-clock/12689 +status: Draft +type: Standards Track +category: ERC +created: 2023-01-25 +--- + +## Abstract + +Many contracts rely on some clock for enforcing delays and storing historical data. While contracts rely on block numbers, others use timestamps. There is currently no easy way to discover which time-tracking function a contract internally uses. This EIP proposes to standardize an interface for contracts to expose their internal clock and thus improve composability. + +## Motivation + +Many contracts check or store time-related information. For example, timelock contracts enforce a delay before an operation can be executed. Similarly, DAOs enforce a voting period during which stakeholders can approve or reject a proposal. Last but not least, voting tokens often store the history of voting power using timed snapshots. + +Some contracts do time tracking using timestamps while others use block numbers. In some cases, more exotic functions might be used to track time. + +There is currently no interface for an external observer to detect which clock a contract uses. This seriously limits interoperability and forces devs to make assumptions. + +## Specification + +### Methods + +#### clock + +This function returns the current timepoint. It could be `block.timestamp`, `block.number` (or any other **non-decreasing** function) depending on the mode the contract is operating on. + +```yaml +- name: clock + type: function + stateMutability: view + inputs: [] + outputs: + - name: timepoint + type: uint48 +``` + +### CLOCK_MODE + +This function returns a string describing the clock the contract is operating on. + +- If operating using **block number**: + - If the block numbers are those of the `NUMBER` opcode (`0x43`), then this function SHOULD return `mode=blocknumber&from=default`. + - If it is any other block number, then this function SHOULD return `mode=blocknumber&from=`, where `` is a CAIP-2 Blockchain ID such as `eip155:1`. +- If operating using **timestamp**, then this function SHOULD return `mode=timestamp`. +- If operating using any other mode, then this function SHOULD return a unique identifier for the encoded `mode` field. + +Note that when operating using **block number**, the `clock()` is expected to return the value given by the `NUMBER` opcode (`0x43`). In some cases, this can be the block number of another chain (in arbitrum, opcode `0x43` returns the block number of the last recorded operation on the parent chain). A contract can use `from=default` to specify that the block number used is the one provided by the `NUMBER` opcode (`0x43`). If a more explicit description is needed, CAIP-2 blockchain id should be used to identify the corresponding blockchain. + +The return string MUST be formatted like a URL query string (a.k.a. `application/x-www-form-urlencoded`). This allows easy decoding in standard JavaScript with `new URLSearchParams(CLOCK_MODE)`. + +```yaml +- name: CLOCK_MODE + type: function + stateMutability: view + inputs: [] + outputs: + - name: descriptor + type: string +``` + +### Solidity interface + +```sol +interface IERC6372 { + function clock() external view returns (uint48); + function CLOCK_MODE() external view returns (string); +} +``` + +### Expected properties + +- The `clock()` function MUST be non-decreasing. + +## Rationale + +`clock` returns `uint48` as it is largely sufficient for storing realistic values. In timestamp mode, `uint48` will be enough until the year 8921556. Even in block number mode, with 10,000 blocks per second, it would be enough until the year 2861. Using a type smaller than uint256 allows some storage packing of timepoints with other associated values. Greatly reducing the cost of writing and reading from storage. + +Depending on the evolution of the blockchain (particularly layer twos), using a smaller type, such as `uint32` might cause issues fairly quickly. On the other hand, anything bigger than `uint48` is overkill. + +## Security Considerations + +No known security issues. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 8bb8117251c3b3477b3c470d6b7a0035b5ca487b Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Thu, 26 Jan 2023 01:08:23 +1000 Subject: [PATCH 200/274] Update EIP-1898: Move to Review (#6309) * removed dependency on 1474, added safe/finalized string options Signed-off-by: Sally MacFarlane * changed status to review Signed-off-by: Sally MacFarlane Signed-off-by: Sally MacFarlane --- EIPS/eip-1898.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-1898.md b/EIPS/eip-1898.md index d4bb6eed02dd36..0bb8bc4d7c5ae6 100644 --- a/EIPS/eip-1898.md +++ b/EIPS/eip-1898.md @@ -4,11 +4,11 @@ title: Add `blockHash` to defaultBlock methods description: Add `blockHash` option to JSON-RPC methods that currently support defaultBlock parameter. author: Charles Cooper (@charles-cooper) discussions-to: https://ethereum-magicians.org/t/eip-1898-add-blockhash-option-to-json-rpc-methods-that-currently-support-defaultblock-parameter/11757 -status: Draft +status: Review type: Standards Track category: Interface created: 2019-04-01 -requires: 234, 1474 +requires: 234 --- ## Abstract @@ -32,8 +32,10 @@ The following options, quoted from the Ethereum JSON-RPC spec, are currently pos > - HEX String - an integer block number > - String "earliest" for the earliest/genesis block -> - String "latest" - for the latest mined block +> - String "latest" - for the latest canonical block > - String "pending" - for the pending state/transactions +> - String "safe" - for the most recent safe block +> - String "finalized" - for the most recent finalized block Since there is no way to clearly distinguish between a DATA parameter and a QUANTITY parameter, this EIP proposes a new scheme for the block parameter. The following option is additionally allowed: From a72a2ca96d5786c0cf94ad69f0097c7c3e5ea678 Mon Sep 17 00:00:00 2001 From: Ignacio Mazzara Date: Wed, 25 Jan 2023 16:10:39 +0100 Subject: [PATCH 201/274] Update EIP-4955: Move to Last Call (#6329) * feat: add eip 4899 * feat: move EIP to last call * feat: wrong file * fix: images and deadline --- EIPS/eip-4955.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-4955.md b/EIPS/eip-4955.md index 87e045447fa20c..f9be560bf80908 100644 --- a/EIPS/eip-4955.md +++ b/EIPS/eip-4955.md @@ -4,7 +4,8 @@ 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: Review +status: Last Call +last-call-deadline: 2023-02-01 type: Standards Track category: ERC created: 2022-03-29 @@ -98,8 +99,7 @@ The main differences between the projects that are rendering 3d NFTs (models) ar 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 - +![](../assets/eip-4955/different-renders.jpeg) ### Metadata (Representations Files) From 87f5d840004e3e65bd08e50423f764b4bd0f10b1 Mon Sep 17 00:00:00 2001 From: Benjamin Evans Chodroff <4411206+benjaminchodroff@users.noreply.github.com> Date: Thu, 26 Jan 2023 00:11:10 +0900 Subject: [PATCH 202/274] Update EIP-4736: Move to Draft (#6304) * Updated to Set Withdrawal Address and ethdo format * ethdo spec format * Remove Acceptance in favor of simplified broadcast file * Removing github external link * Markdown Linter fixes * Lint Unordered list style and code blocks --- EIPS/eip-4736.md | 190 +++++++++++++++++++++-------------------------- 1 file changed, 86 insertions(+), 104 deletions(-) diff --git a/EIPS/eip-4736.md b/EIPS/eip-4736.md index 4375d8d9b4cb74..4fbb85965586a0 100644 --- a/EIPS/eip-4736.md +++ b/EIPS/eip-4736.md @@ -1,186 +1,167 @@ --- eip: 4736 title: Consensus Layer Withdrawal Protection -description: Additional security for "change withdrawal address" operation when a consensus layer mnemonic may be compromised, without changing consensus +description: Additional security for "set withdrawal address" operation when a consensus layer mnemonic may be compromised, without changing consensus author: Benjamin Chodroff (@benjaminchodroff), Jim McDonald (@mcdee) discussions-to: https://ethereum-magicians.org/t/consensus-layer-withdrawal-protection/8161 -status: Stagnant +status: Draft type: Standards Track category: Interface created: 2022-01-30 --- ## Abstract + If a consensus layer mnemonic phrase is compromised, it is impossible for the consensus layer network to differentiate the "legitimate" holder of the key from an "illegitimate" holder. However, there are signals that can be considered in a wider sense without changing core Ethereum consensus. This proposal outlines ways in which the execution layer deposit address, a consensus layer rebroadcast delay, and list of signed messages could create a social consensus that would significantly favor but not guarantee legitimate mnemonic holders would win a race condition against an attacker, while not changing core Ethereum consensus. ## Motivation -The consensus layer change withdrawal credentials proposal is secure for a single user who has certainty their keys and mnemonic have not been compromised. However, as validator withdrawals on the consensus layer are not yet possible, no user can have absolute certainty that their keys are not compromised until the change withdrawal address is on chain, and by then too late to change. All legitimate mnemonic phrase holders were originally in control of the execution layer deposit address. Beacon node clients and node operators may optionally load a list of verifiable deposit addresses, a list of verifiable change withdrawal address messages to broadcasts, and specify a rebroadcast delay that may create a social consensus for legitimate holders to successfully win a race condition against an attacker. If attackers compromise a significant number of consensus layer nodes, it would pose risks to the entire Ethereum community. -Setting a withdrawal address to an execution layer address was not supported by the eth2.0-deposit-cli until v1.1.1 on March 23, 2021, leaving early adopters wishing they could force change their execution layer address earlier. Forcing this change is not something that can be enforced in-protocol, partly due to lack of information on the beacon chain about the execution layer deposit address and partly due to the fact that this was never listed as a requirement. It is also possible that the execution layer deposit address is no longer under the control of the legitimate holder of the withdrawal private key. +The consensus layer set withdrawal address proposal is secure for a single user who has certainty their keys and mnemonic have not been compromised. However, as validator withdrawals on the consensus layer are not yet possible, no user can have absolute certainty that their keys are not compromised until the set withdrawal address is on chain, and by then too late to change. All legitimate mnemonic phrase holders were originally in control of the execution layer deposit address. Beacon node clients and node operators may optionally load a list of verifiable deposit addresses, a list of verifiable set withdrawal address messages to broadcasts, and specify a rebroadcast delay that may create a social consensus for legitimate holders to successfully win a race condition against an attacker. If attackers compromise a significant number of consensus layer nodes, it would pose risks to the entire Ethereum community. + +Setting a withdrawal address to an execution layer address was not supported by the eth2.0-deposit-cli until v1.1.1 on March 23, 2021, leaving early adopters wishing they could force set their execution layer address earlier. Forcing this change is not something that can be enforced in-protocol, partly due to lack of information on the beacon chain about the execution layer deposit address and partly due to the fact that this was never listed as a requirement. It is also possible that the execution layer deposit address is no longer under the control of the legitimate holder of the withdrawal private key. -However, it is possible for individual nodes to locally restrict the changes they wish to include in blocks they propose, and which they propagate around the network, in a way that does not change consensus. It is also possible for client nodes to help broadcast signed change withdrawal address requests to ensure as many nodes witness this change as soon as possible in a fair manner. Further, such change withdrawal address signed messages can be preloaded into clients in advance to further help nodes filter attacking requests. +However, it is possible for individual nodes to locally restrict the changes they wish to include in blocks they propose, and which they propagate around the network, in a way that does not change consensus. It is also possible for client nodes to help broadcast signed set withdrawal address requests to ensure as many nodes witness this message as soon as possible in a fair manner. Further, such set withdrawal address signed messages can be preloaded into clients in advance to further help nodes filter attacking requests. -This proposal provides purely optional additional protection. It aims to request nodes set a priority on withdrawal credential claims that favour a verifiable execution layer deposit address in the event of two conflicting change withdrawal credentials. It also establishes a list of change withdrawal address signed messages to help broadcast "as soon as possible" when the network supports it, and encourage client teams to help use these lists to honour filter and prioritize accepting requests by REST and transmitting them via P2P. This will not change consensus, but may help prevent propagating an attack where a withdrawal key has been knowingly or unknowingly compromised. +This proposal provides purely optional additional protection. It aims to request nodes set a priority on withdrawal credential claims that favour a verifiable execution layer deposit address in the event of two conflicting set withdrawal credentials. It also establishes a list of set withdrawal address signed messages to help broadcast "as soon as possible" when the network supports it, and encourage client teams to help use these lists to honour filter and prioritize accepting requests by REST and transmitting them via P2P. This will not change consensus, but may help prevent propagating an attack where a withdrawal key has been knowingly or unknowingly compromised. -It is critical to understand that this proposal is not a consensus change. Nothing in this proposal restricts the validity of withdrawal credential change operations within the protocol. It is a voluntary change by client teams to build this functionality in to their beacon nodes, and a voluntary change by node operators to accept any or all of the restrictions and broadcasting capabilities suggested by end users. +It is critical to understand that this proposal is not a consensus change. Nothing in this proposal restricts the validity of withdrawal credential operations within the protocol. It is a voluntary change by client teams to build this functionality in to their beacon nodes, and a voluntary change by node operators to accept any or all of the restrictions and broadcasting capabilities suggested by end users. Because of the above, even if fully implemented, it will be down to chance as to which validators propose blocks, and which voluntary restrictions those validators’ beacon nodes are running. Node operators can do what they will to help the community prevent attacks on any compromised consensus layer keys, but there are no guarantees of success this will prevent a successful attack. ## Specification -The Consensus Layer change withdrawal credentials operation is not yet fully specified, but MUST have at least the following fields: + +The Consensus Layer set withdrawal credentials operation MUST have at least the following fields: + * Validator index -* Current withdrawal public key +* Current withdrawal BLS public key * Proposed execution layer withdrawal address * Signature by withdrawal private key over the prior fields This proposal describes three OPTIONAL and RECOMMENDED mechanisms which a client beacon node MAY implement, and end users are RECOMMENDED to use in their beacon node operation. -### 1. Change Withdrawal Address Acceptance File -Beacon node clients MAY support an OPTIONAL file in the format "withdrawal credentials, execution layer address" which, if implemented and if provided, SHALL allow clients to load, or RECOMMENDED packaged by default, a verifiable list matching the consensus layer withdrawal credentials and the original execution layer deposit address. While any withdrawal credential and execution layer address found in the file SHALL be supported, this list MAY be used to help enforce a deposit address is given preference in rebroadcasting, even if other clients do not support or have not loaded an OPTIONAL "Change Withdrawal Address Broadcast" file. +### 1. Set Withdrawal Address Broadcast File -### 2. Change Withdrawal Address Broadcast File -Beacon node clients MAY support an OPTIONAL file of lines specifying "validator index,current withdrawal public key,proposed execution layer withdrawal address,consensus layer signature" which, if implemented and if provided, SHALL instruct nodes to automatically submit a one-time change withdrawal address broadcast message for each valid line at the block height the network supports a "change withdrawal address" operation. This file SHALL give all node operators an OPTIONAL opportunity to ensure any valid change withdrawal address messages are broadcast, heard, and shared by nodes during the first epoch supporting the change withdrawal address operation. This OPTIONAL file SHALL also instruct nodes to perpetually prefer accepting and repeating signatures matching the signature in the file, and SHALL reject accepting or rebroadcasting messages which do not match a signature for a given withdrawal credential. +Beacon node clients MAY support an OPTIONAL file of lines specifying "validator index" , "current withdrawal BLS public key" , "proposed execution layer withdrawal address", and "signature" which, if implemented and if provided, SHALL instruct nodes to automatically submit a one-time set withdrawal address broadcast message for each valid signature at the block height the network supports a "set withdrawal address" operation. This file SHALL give all node operators an OPTIONAL opportunity to ensure any valid set withdrawal address messages are broadcast, heard, and shared by nodes during the first epoch supporting the set withdrawal address operation. This OPTIONAL file SHALL also instruct nodes to perpetually prefer accepting and repeating signatures matching the signature in the file, and SHALL reject accepting or rebroadcasting messages which do not match a signature for a given withdrawal credential. -### 3. Change Withdrawal Address Rebroadcast Delay -Beacon node clients MAY implement an OPTIONAL time measurement parameter "change withdrawal address rebroadcast delay" that, if implemented and if provided, SHALL create a delay in rebroadcasting change withdrawal addresses (RECOMMENDED to default to 2000 seconds (>5 epochs), or OPTIONAL set to 0 seconds for no delay, or MAY set to -1 to strictly only rebroadcast requests matching a "Change Withdrawal Address Broadcast File" signature or "Change Withdrawal Address Acceptance File" entry). This setting SHALL allow change withdrawal address requests time for peer replication of client accepted valid requests that are preferred by the community. This MAY prevent a "first to arrive" critical race condition for a conflicting change withdraw address. +### 2. Set Withdrawal Address Rebroadcast Delay + +Beacon node clients MAY implement an OPTIONAL time measurement parameter "set withdrawal address rebroadcast delay" that, if implemented and if provided, SHALL create a delay in rebroadcasting set withdrawal addresses (RECOMMENDED to default to 2000 seconds (>5 epochs), or OPTIONAL set to 0 seconds for no delay, or MAY set to -1 to strictly only rebroadcast requests matching a "Set Withdrawal Address Broadcast" entry). This setting SHALL allow set withdrawal address requests time for peer replication of client accepted valid requests that are preferred by the community. This MAY prevent a "first to arrive" critical race condition for a conflicting set withdraw address. -### Change Withdrawal Address Handling -Beacon node clients are RECOMMENDED to first rely on a "Change Withdrawal Address Broadcast" file of verifiable signatures, then MAY fallback to a "Change Withdrawal Address Acceptance" file intended to be loaded with all validator original deposit address information, and then MAY fallback to accept a "first request" but delay in rebroadcasting it via P2P. All of this proposal is OPTIONAL for beacon nodes to implement or use, but all client teams are RECOMMENDED to include a copy or link to the uncontested verification file and RECOMMENDED enable it by default to protect the entire Ethereum community. This OPTIONAL protection will prove the user was both in control of the consensus layer and execution layer address, while making sure their intended change withdrawal address message is ready to broadcast as soon as the network supports it. +### Set Withdrawal Address Handling -If a node is presented with a change withdrawal address operation via the REST API or P2P, they are RECOMMENDED to follow this process: +Beacon node clients are RECOMMENDED to first rely on a "Set Withdrawal Address Broadcast" file of verifiable signatures, and then MAY fallback to accept a "first request" but delay in rebroadcasting it via P2P. All of this proposal is OPTIONAL for beacon nodes to implement or use, but all client teams are RECOMMENDED to include a copy or link to the uncontested verification file and RECOMMENDED enable it by default to protect the entire Ethereum community. This OPTIONAL protection will prove the user was both in control of the consensus layer and execution layer address, while making sure their intended set withdrawal address message is ready to broadcast as soon as the network supports it. -A) Withdrawal credential found in "Change Withdrawal Address Broadcast" file: - 1. Signature Match: If a valid change withdrawal request signature is received for a withdrawal credential that matches the first signature found in the "Change Withdrawal Address Broadcast" file, accept it via REST API, rebroadcast it via P2P, and drop any pending “first preferred” if existing. - 2. Signature Mismatch: If a valid change withdrawal request is received for a withdrawal credential that does not match the first signature found in the "Change Withdrawal Address Broadcast" file, reject it via REST API, and drop it to prevent rebroadcasting it via P2P. +If a node is presented with a set withdrawal address operation via the REST API or P2P, they are RECOMMENDED to follow this process: -B) Withdrawal credential not found in or no "Change Withdrawal Address Broadcast" file provided, or capability not implemented in the client: -1. Matching withdraw credential and withdraw address in "Change Withdrawal Address Acceptance" file: If a valid change withdrawal address request is received for a withdrawal credential that matches the first found withdrawal address provided in the "Change Withdrawal Address Acceptance" file, accept it via REST API, rebroadcast it via P2P, and drop any pending “first preferred” if existing. -2. Mismatching withdraw credential and withdraw address in "Change Withdrawal Address Acceptance" file: If a valid change withdrawal request is received for a withdrawal credential that does not match the first found withdrawal address provided in the "withdrawal address" file, reject it via REST API, and drop it to prevent rebroadcasting it via P2P. -3. Missing withdraw address in or no "Change Withdrawal Address Acceptance" file: +A) Withdrawal credential found in "Set Withdrawal Address Broadcast" file: - i. First Preferred: If first valid change withdrawal request is received for a not finalized withdrawal credential that does not have any listed withdrawal credential entry in the "Change Withdrawal Address Acceptance" file, accept it via REST API, but do not yet rebroadcast it via P2P (“grace period”) and do not yet propose any local blocks with this message. Once the client “Change Withdrawal Address Grace Period” has expired and no other messages have invalidated this message, rebroadcast the request via P2P and include in locally built blocks if not already present. + 1. Signature Match: If a valid set withdrawal request signature is received for a withdrawal credential that matches the first signature found in the "Set Withdrawal Address Broadcast" file, accept it via REST API, rebroadcast it via P2P, and drop any pending “first preferred” if existing. + 2. Signature Mismatch: If a valid set withdrawal request is received for a withdrawal credential that does not match the first signature found in the "Set Withdrawal Address Broadcast" file, reject it via REST API, and drop it to prevent rebroadcasting it via P2P. + +B) Withdrawal credential not found in or no "Set Withdrawal Address Broadcast" file provided, or capability not implemented in the client: + +1. Matching withdraw credential and withdraw address in "Set Withdrawal Address Broadcast" file: If a valid set withdrawal address request is received for a withdrawal credential that matches the first found withdrawal address provided in the "Set Withdrawal Address Broadcast" file, accept it via REST API, rebroadcast it via P2P, and drop any pending “first preferred” if existing. +2. Mismatching withdraw credential and withdraw address in "Set Withdrawal Address Broadcast" file: If a valid set withdrawal request is received for a withdrawal credential that does not match the first found withdrawal address provided in the "withdrawal address" file, reject it via REST API, and drop it to prevent rebroadcasting it via P2P. +3. Missing withdraw address in or no "Set Withdrawal Address Broadcast" file: + + i. First Preferred: If first valid set withdrawal address request is received for a not finalized withdrawal credential that does not have any listed withdrawal credential entry in the "Set Withdrawal Address Broadcast" file, accept it via REST API, but do not yet rebroadcast it via P2P (“grace period”) and do not yet propose any local blocks with this message. Once the client “Set Withdrawal Address Grace Period” has expired and no other messages have invalidated this message, rebroadcast the request via P2P and include in locally built blocks if not already present. ii. Subsequent Rejected: If an existing valid "First Preferred" request exists for a not finalized withdrawal credential, reject it via REST API, and drop it to prevent rebroadcasting via P2P. -Note that these restrictions SHALL NOT apply to withdrawal credential change operations found in blocks. If any operation has been included on-chain, it MUST by definition be valid regardless of its contents or protective mechanisms described above. +Note that these restrictions SHALL NOT apply to set withdrawal credential operations found in blocks. If any operation has been included on-chain, it MUST by definition be valid regardless of its contents or protective mechanisms described above. ## Rationale -This proposal is intended to protect legitimate mnemonic phrase holders where the phrase was knowingly or unknowingly compromised. As there is no safe way to transfer ownership of a validator without exiting, it can safely be assumed that all current validator holders intend to change to a withdrawal address they specify. Using the deposit address in the execution layer to determine the legitimate holder is not possible to consider in consensus as it may be far back in history and place an overwhelming burden to maintain such a list. As such, this proposal outlines optional mechanism which protect legitimate original mnemonic holders and does so in a way that does not place any mandatory burden on client node software or operators. + +This proposal is intended to protect legitimate mnemonic phrase holders where the phrase was knowingly or unknowingly compromised. As there is no safe way to transfer ownership of a validator without exiting, it can safely be assumed that all current validator holders intend to set to a withdrawal address they specify. Using the deposit address in the execution layer to determine the legitimate holder is not possible to consider in consensus as it may be far back in history and place an overwhelming burden to maintain such a list. As such, this proposal outlines optional mechanism which protect legitimate original mnemonic holders and does so in a way that does not place any mandatory burden on client node software or operators. ## Backwards Compatibility -As there is currently no existing "change withdrawal address" operation in existence, there is no documented backwards compatibility. As all of the proposal is OPTIONAL in both implementation and operation, it is expected that client beacon nodes that do not implement this functionality would still remain fully backwards compatible with any or all clients that do implement part or all of the functionality described in this proposal. Additionally, while users are RECOMMENDED to enable these OPTIONAL features, if they decide to either disable or ignore some or all of the features, or even purposefully load content contrary to the intended purpose, the beacon node client will continue to execute fully compatible with the rest of the network as none of the proposal will change core Ethereum consensus. + +As there is currently no existing "set withdrawal address" operation, there is no documented backwards compatibility. As all of the proposal is OPTIONAL in both implementation and operation, it is expected that client beacon nodes that do not implement this functionality would still remain fully backwards compatible with any or all clients that do implement part or all of the functionality described in this proposal. Additionally, while users are RECOMMENDED to enable these OPTIONAL features, if they decide to either disable or ignore some or all of the features, or even purposefully load content contrary to the intended purpose, the beacon node client will continue to execute fully compatible with the rest of the network as none of the proposal will change core Ethereum consensus. ## Reference Implementation -### Change Withdrawal Address Acceptance File -A file intended to be preloaded with all consensus layer withdrawal credentials and verifiable execution layer deposit addresses. This file will be generated by a script and able to be independently verified by community members using the consensus and execution layers, and intended to be included by all clients, enabled by default. Client nodes are encouraged to enable packaging this independently verifiable list with the client software, and enable it by default to help further protect the community from unsuspected attacks. -depositAddress.csv format (both fields required): -`withdrawal credential, withdrawal address` +### Set Withdrawal Address Broadcast File -Example depositAddress.csv: -``` -000092c20062cee70389f1cb4fa566a2be5e2319ff43965db26dbaa3ce90b9df99,01c34eb7e3f34e54646d7cd140bb7c20a466b3e852 -0000d66cf353931500a54cbd0bc59cbaac6690cb0932f42dc8afeddc88feeaad6f,01c34eb7e3f34e54646d7cd140bb7c20a466b3e852 -0000d6b91fbbce0146739afb0f541d6c21e8c41e92b97874828f402597bf530ce4,01c34eb7e3f34e54646d7cd140bb7c20a466b3e852 -000037ca9a1cf2223d8b9f81a14d4937fef94890ae4fcdfbba928a4dc2ff7fcf3b,01c34eb7e3f34e54646d7cd140bb7c20a466b3e852 -0000344b6c73f71b11c56aba0d01b7d8ad83559f209d0a4101a515f6ad54c89771,01f19b1c91faacf8071bd4bb5ab99db0193809068e -``` +A "change-operations.json" file intended to be preloaded with all consensus layer withdrawal credential signatures and verifiable execution layer deposit addresses. This file may be generated by a script and able to be independently verified by community members using the consensus layer node, and intended to be included by all clients, enabled by default. Client nodes are encouraged to enable packaging this independently verifiable list with the client software, and enable it by default to help further protect the community from unsuspected attacks. + +The change-operations.json format is the "Set Withdrawal Address File - Claim" combined into a single JSON array. -### Change Withdrawal Address Broadcast File - Claim -A community collected and independently verifiable list of "Change Withdrawal Address Broadcasts" containing verifiable claims will be collected. Client teams and node operators may verify these claims independently and are suggested to include "Uncontested and Verified" claims enabled by default in their package. +### Set Withdrawal Address Broadcast File - Claim -To make a verifiable claim, users must upload using their GitHub ID with the following contents to the [CLWP repository](https://github.com/benjaminchodroff/ConsensusLayerWithdrawalProtection) in a text file "claims/validatorIndex-gitHubUser.txt" such as "123456-myGitHubID.txt" +A community collected and independently verifiable list of "Set Withdrawal Address Broadcasts" containing verifiable claims will be collected. Client teams and node operators may verify these claims independently and are suggested to include "Uncontested and Verified" claims enabled by default in their package. + +To make a verifiable claim, users MAY upload using their GitHub ID with the following contents to the CLWP repository in a text file "[chain]/validatorIndex.json" such as "mainnet/123456.json" or MAY construct a repository of their own. + +123456.json: -123456-myGitHubID.txt: ``` -current_withdrawal_public_key=b03c5ea17b017cffd22b6031575c4453f20a4737393de16a626fb0a8b0655fe66472765720abed97e8022680204d3868 -proposed_withdrawal_address=0108f2e9Ce74d5e787428d261E01b437dC579a5164 -consensus_layer_withdrawal_signature= -execution_layer_deposit_signature= -execution_layer_withdrawal_signature= -email=noreply@ethereum.org +[{"message":{"validator_index":"123456","from_bls_pubkey":"0x977cc21a067003e848eb3924bcf41bd0e820fbbce026a0ff8e9c3b6b92f1fea968ca2e73b55b3908507b4df89eae6bfb","to_execution_address":"0x08f2e9Ce74d5e787428d261E01b437dC579a5164"},"signature":"0x872935e0724b31b2f0209ac408b673e6fe2f35b640971eb2e3b429a8d46be007c005431ef46e9eb11a3937f920cafe610c797820ca088543c6baa0b33797f0a38f6db3ac68ffc4fd03290e35ffa085f0bfd56b571d7b2f13c03f7b6ce141c283"}] ``` -| Key | Value | -| ----| ------| -| **current_withdrawal_public_key** | (Required) The "pubkey" field found in deposit_data json file matching the validator index| Required | Necessary for verification | -| **proposed_withdrawal_address** | (Required) The address in Ethereum you wish to authorize withdrawals to, prefaced by "01" without any "0x", such that an address "0x08f2e9Ce74d5e787428d261E01b437dC579a5164" turns into "0108f2e9Ce74d5e787428d261E01b437dC579a5164 | -| **consensus_layer_withdrawal_signature** | (Required) The verifiable signature generated by signing "validator_index,current_withdrawal_public_key,proposed_withdrawal_address" using the consensus layer private key | -| execution_layer_deposit_signature | (Optional) The verifiable signature generated by signing "validator_index,current_withdrawal_public_key,proposed_withdrawal_address" using the execution layer deposit address private key | -| execution_layer_withdrawal_signature | (Optional) The verifiable signature generated by signing "validator_index,current_withdrawal_public_key,proposed_withdrawal_address" using the execution layer proposed withdrawal address private key. This may be the same result as the "execution_layer_deposit_signature" if the user intends to withdraw to the same execution layer deposit address. | -| email | (Optional) Any actively monitored email address to notify if contested | - #### Claim Acceptance -In order for a submission to be merged into CLWP GitHub repository, the submission must have: -1. Valid filename in the format validatorIndex-githubUsername.txt -2. Valid validator index which is deposited, pending, or active on the consensus layer -3. Matching GitHub username in file name to the user submitting the request -4. Verifiable consensus_layer_withdrawal_signature, and a verifiable execution_layer_deposit_signature and execution_layer_withdrawal_signature if included -5. All required fields in the file with no other content present -All merge requests that fail will be provided a reason from above which must be addressed prior to merge. Any future verifiable amendments to accepted claims must be proposed by the same GitHub user, or it will be treated as a contention. +In order for a submission to be merged into CLWP GitHub repository, the submission must have: -#### Change Withdrawal Address Broadcast -Anyone in the community will be able to generate the following verifiable files from the claims provided: - A. UncontestedVerified - Community collected list of all verifiable uncontested change withdrawal address final requests (no conflicting withdrawal credentials allowed from different GitHub users) - B. ContestedVerified - Community collected list of all contested verifiable change withdrawal address requests (will contain only verifiable but conflicting withdrawal credentials from different GitHub users) +1. Valid filename in the format validatorIndex.json +2. Valid validator index which is active on the consensus layer +3. Verifiable signature +5. A single change operation for a single validator, with all required fields in the file with no other content present -A claim will be considered contested if a claim arrives where the verifiable consensus layer signatures differ between two or more GitHub submissions, where neither party has proven ownership of the execution layer deposit address. If a contested but verified "Change Withdrawal Address Broadcast" request arrives to the GitHub community, all parties will be notified via GitHub, forced into the ContestedVerified list, and may try to convince the wider community by providing any off chain evidence supporting their claim to then include their claim in nodes. All node operators are encouraged to load the UncontestedVerified signatures file as enabled, and optionally append only ContestedVerified signatures that they have been convinced are the rightful owner in a manner to further strengthen the community. +All merge requests that fail will be provided a reason from above which must be addressed prior to merge. Any future verifiable amendments to accepted claims must be proposed by the same GitHub user, or it will be treated as a contention. -The uncontested lists will be of the format: +#### Set Withdrawal Address Broadcast -UncontestedVerified-datetime.txt: -``` -validator_index,current_withdrawal_public_key,proposed_withdrawal_address,consensus_layer_withdrawal_signature -``` +Anyone in the community will be able to independently verify the files from the claims provided using the Capella spec and command line clients such as "ethdo" which support the specification. -The contested list will be of the format: - -ContestedVerified-datetime.txt -``` -validator_index,current_withdrawal_public_key,proposed_withdrawal_address,consensus_layer_withdrawal_signature,email -``` +A claim will be considered contested if a claim arrives where the verifiable consensus layer signatures differ between two or more GitHub submissions, where neither party has proven ownership of the execution layer deposit address. If a contested but verified "Set Withdrawal Address Broadcast" request arrives to the GitHub community, all parties will be notified via GitHub, and may try to convince the wider community by providing any publicly verifiable on chain evidence or off chain evidence supporting their claim to then include their claim in nodes. Node operators may decide which verifiable claims they wish to include based on social consensus. ## Security Considerations ### 1: Attacker lacks EL deposit key, uncontested claim -- User A: Controls the CL keys and the EL key used for the deposit -- User B: Controls the CL keys, but does not control the EL key for the deposit -User A signs and submits a claim to the CLWP repository, clients load User A message into the "Change Withdrawal Address Broadcast" file. At the time of the first epoch support Change Withdrawal Address, many (not all) nodes begin to broadcast the message. User B also tries to submit a different but valid Change Withdrawal Address to an address that does not match the signature in the claim. This message is successfully received via REST API, but some (not all) nodes begin to silently drop this message as the signature does not match the signature in the "Change Withdrawal Address Broadcast" file. As such, these nodes do not replicate this message via P2P. The nodes which do not have a Change Withdrawal Address Broadcast file loaded may still impose a "Change Withdrawal Address Rebroadcast Delay" to keep listening (for about 5 epochs) to see if there are any conflicts to this message. This delay may give User A an advantage in beating User B to consensus, but there is no certainty as it will depend on chance which validator and nodes are involved. +* User A: Controls the CL keys and the EL key used for the deposit +* User B: Controls the CL keys, but does not control the EL key for the deposit + +User A signs and submits a claim to the CLWP repository, clients load User A message into the "Set Withdrawal Address Broadcast" file. At the time of the first epoch support Set Withdrawal Address, many (not all) nodes begin to broadcast the message. User B also tries to submit a different but valid Set Withdrawal Address to an address that does not match the signature in the claim. This message is successfully received via REST API, but some (not all) nodes begin to silently drop this message as the signature does not match the signature in the "Set Withdrawal Address Broadcast" file. As such, these nodes do not replicate this message via P2P. The nodes which do not have a Set Withdrawal Address Broadcast file loaded may still impose a "Set Withdrawal Address Rebroadcast Delay" to keep listening (for about 5 epochs) to see if there are any conflicts to this message. This delay may give User A an advantage in beating User B to consensus, but there is no certainty as it will depend on chance which validator and nodes are involved. ### 2: Attacker has both EL deposit key and CL keys, uncontested claim -- User A: Controls the CL key/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address -- User B: Controls the CL and EL key/mnemonic used for the EL deposit, but fails to submit a claim + +* User A: Controls the CL key/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address +* User B: Controls the CL and EL key/mnemonic used for the EL deposit, but fails to submit a claim It is possible/likely that User A would notice that all their funds in the EL deposit address had been stolen. This may signal that their CL key is compromised as well, so they decide to pick a new address for the withdrawal. The story will play out the same as Scenario 1 as the claim is uncontested. ### 3: Same as #2, but the attacker submits a contested claim -- User A: Controls the CL keys/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address -- User B: Controls the CL keys/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address -This is a contested claim and as such there is no way to prove who is in control using on chain data. Instead, either user may try to persuade the community they are the rightful owner (identity verification, social media, etc.) in an attempt to get node operators to load their contested claim into their "Change Withdrawal Address Broadcast" file. However, there is no way to fully prove it. +* User A: Controls the CL keys/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address +* User B: Controls the CL keys/mnemonic and the EL key used for the deposit, and submits a claim to move to a new address + +This is a contested claim and as such there is no way to prove who is in control using on chain data. Instead, either user may try to persuade the community they are the rightful owner (identity verification, social media, etc.) in an attempt to get node operators to load their contested claim into their "Set Withdrawal Address Broadcast" file. However, there is no way to fully prove it. ### 4: A user has lost either their CL key and/or mnemonic (no withdrawal key) -- User A: Lacks the CL keys and mnemonic + +* User A: Lacks the CL keys and mnemonic There is no way to recover this scenario with this proposal as we cannot prove a user has lost their keys, and the mnemonic is required to generate the withdrawal key. ### 5: End game - attacker -- User A: Controls EL and CL key/mnemonic, successfully achieves a change address withdrawal -- User B: Controls CL key, decides to attack -Upon noticing User A has submitted a successful change address withdrawal, User B may run a validator and attempt to get User A slashed +* User A: Controls EL and CL key/mnemonic, successfully achieves a set address withdrawal +* User B: Controls CL key, decides to attack + +Upon noticing User A has submitted a successful set address withdrawal, User B may run a validator and attempt to get User A slashed ### 6: Compromised key, but not vulnerable to withdrawal -- User A: Controls EL and CL key/mnemonic, but has a vulnerability which leaks their CL key but NOT their CL mnemonic -- User B: Controls the CL key, but lacks the CL mnemonic + +* User A: Controls EL and CL key/mnemonic, but has a vulnerability which leaks their CL key but NOT their CL mnemonic +* User B: Controls the CL key, but lacks the CL mnemonic User A may generate the withdrawal key (requires the mnemonic). User B can attack User A by getting them slashed, but will be unable to generate the withdrawal key. -### 7: Attacker loads a malicious Change Withdrawal Address Broadcast and Change Withdrawal Address Acceptance files into one or multiple nodes, User A submits claim -- User A: Submits a valid uncontested claim which is broadcast out as soon as possible by many nodes -- User B: Submits no claim, but broadcasts a valid malicious claim out through their Change Withdrawal Address Broadcast list, and blocks User A's claim from their node. +### 7: Attacker loads a malicious Set Withdrawal Address Broadcast file into one or multiple nodes, User A submits claim + +* User A: Submits a valid uncontested claim which is broadcast out as soon as possible by many nodes +* User B: Submits no claim, but broadcasts a valid malicious claim out through their Set Withdrawal Address Broadcast list, and blocks User A's claim from their node. User B's claim will make it into many nodes, but when it hits nodes that have adopted User A's signature they will be dropped and not rebroadcast. Statistically, User B will have a harder time achieving consensus among the entire community, but it will be down to chance. @@ -189,10 +170,11 @@ User B's claim will make it into many nodes, but when it hits nodes that have ad The attacker will statistically likely win as they will be first to have their message broadcast to many nodes and, unless User A submits a request exactly at the time of support, it is unlikely to be heard by enough nodes to gain consensus. All users are encouraged to submit claims for this reason because nobody can be certain their mnemonic has not been compromised until it is too late. ### Second Order Effects -1. A user who participates in the "Change Withdrawal Address Broadcast" may cause the attacker to give up early and instead start to slash. For some users, the thought of getting slashed is preferable to giving an adversary any funds. As the proposal is voluntary, users may choose not to participate if they fear this scenario. -2. The attacker may set up their own unverified list of their own Change Withdrawal Address Acceptance file and nodes adopting this list to break ties in their favour. It is unlikely they would operate enough beacon nodes to form a consensus. -3. The attacker may set up their own Change Withdrawal Address Broadcast to reject signatures not matching their attack. This is possible with or without this proposal. -4. The attacker may be the one collecting "Change Withdrawal Address Broadcast" claims for this proposal and may purposefully reject legitimate requests. Anyone is free to set up their own community claim collection and gather their own community support using the same mechanisms described in this proposal to form an alternative social consensus. Come at me bro. + +1. A user who participates in the "Set Withdrawal Address Broadcast" may cause the attacker to give up early and instead start to slash. For some users, the thought of getting slashed is preferable to giving an adversary any funds. As the proposal is voluntary, users may choose not to participate if they fear this scenario. +2. The attacker may set up their own Set Withdrawal Address Broadcast to reject signatures not matching their attack. This is possible with or without this proposal. +3. The attacker may be the one collecting "Set Withdrawal Address Broadcast" claims for this proposal and may purposefully reject legitimate requests. Anyone is free to set up their own community claim collection and gather their own community support using the same mechanisms described in this proposal to form an alternative social consensus. Come at me bro. ## Copyright + Copyright and related rights waived via [CC0](../LICENSE.md). From 318d5684602d0a43df762ee08306bc4cf6d0151b Mon Sep 17 00:00:00 2001 From: Daniel Tedesco Date: Wed, 25 Jan 2023 23:32:01 +0800 Subject: [PATCH 203/274] Update EIP-4974: Move to Review (#6334) * Apply suggestions from initial review co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> update links Apply initial suggestions from code review Update EIP number and typos. Co-authored-by: William Schwab <31592931+wschwab@users.noreply.github.com> Co-authored-by: lightclient <14004106+lightclient@users.noreply.github.com> rename, add enumerable, update language update name conventions, ERC165 identifier update backwards compatability * minor wording edit * update with participate and transfer nomenclature * update reference links * update reference to assets folder * removing the assets link * small update * begin update to rating * Apply suggestions from code review * Full update to Ratings * Fix links * Update interface and add implementation example * Update EIPS/eip-4974.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Update EIPS/eip-4974.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Remove self-references * Fix bot errors * Last bot errors * Update EIP-5570: Move to Review After community feedback and the draft process, this EIP is ready for the Review stage. * Fix license Co-authored-by: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-4974.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-4974.md b/EIPS/eip-4974.md index 4334f4a86c6ce4..304bbebb25a0fe 100644 --- a/EIPS/eip-4974.md +++ b/EIPS/eip-4974.md @@ -4,7 +4,7 @@ title: Ratings description: An interface for assigning and managing numerical ratings author: Daniel Tedesco (@dtedesco1) discussions-to: https://ethereum-magicians.org/t/8805 -status: Draft +status: Review type: Standards Track category: ERC created: 2022-04-02 @@ -158,4 +158,4 @@ Overall, the security of compliant contracts will depend on the careful manageme ## Copyright -Copyright and related rights waived via CC0. +Copyright and related rights waived via [CC0](../LICENSE.md). From cf35f4c7097dd12618a741c5eb69199e0c72e6df Mon Sep 17 00:00:00 2001 From: Jan Turk Date: Wed, 25 Jan 2023 17:45:12 +0100 Subject: [PATCH 204/274] Purge EIP-6220 of bloated asset (#6369) A ReentrancyGuard guard was removed from the example implementation of EIP-6220. Two references in the Specification were updated. --- EIPS/eip-6220.md | 4 +- assets/eip-6220/contracts/EquippableToken.sol | 4 +- .../contracts/security/ReentrancyGuard.sol | 77 ------------------- 3 files changed, 3 insertions(+), 82 deletions(-) delete mode 100644 assets/eip-6220/contracts/security/ReentrancyGuard.sol diff --git a/EIPS/eip-6220.md b/EIPS/eip-6220.md index 879b32ce9d327c..f61f15dac27007 100644 --- a/EIPS/eip-6220.md +++ b/EIPS/eip-6220.md @@ -59,7 +59,7 @@ The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL The interface of the core smart contract of the equippable tokens. ```solidity -/// @title EIP-X Composable NFTs utilizing Equippable Parts +/// @title EIP-6220 Composable NFTs utilizing Equippable Parts /// @dev See https://eips.ethereum.org/EIPS/eip-6220 /// @dev Note: the ERC-165 identifier for this interface is 0x28bc9ae4. @@ -265,7 +265,7 @@ The interface of the Catalog containing the equippable parts. Catalogs are colle pragma solidity ^0.8.16; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "./IERC165.sol"; interface ICatalog is IERC165 { /** diff --git a/assets/eip-6220/contracts/EquippableToken.sol b/assets/eip-6220/contracts/EquippableToken.sol index ea60fbca457d05..84e85f30ac904d 100644 --- a/assets/eip-6220/contracts/EquippableToken.sol +++ b/assets/eip-6220/contracts/EquippableToken.sol @@ -8,7 +8,6 @@ import "./ICatalog.sol"; import "./IEquippable.sol"; import "./IERC6059.sol"; import "./library/EquippableLib.sol"; -import "./security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; import "@openzeppelin/contracts/utils/Address.sol"; @@ -67,7 +66,6 @@ error UnexpectedNumberOfChildren(); */ contract EquippableToken is Context, - ReentrancyGuard, IERC165, IERC721, IERC6059, @@ -1753,7 +1751,7 @@ contract EquippableToken is */ function equip( IntakeEquip memory data - ) public virtual onlyApprovedOrOwner(data.tokenId) nonReentrant { + ) public virtual onlyApprovedOrOwner(data.tokenId) { _equip(data); } diff --git a/assets/eip-6220/contracts/security/ReentrancyGuard.sol b/assets/eip-6220/contracts/security/ReentrancyGuard.sol deleted file mode 100644 index 5fab06e46f0c5c..00000000000000 --- a/assets/eip-6220/contracts/security/ReentrancyGuard.sol +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol) - -pragma solidity ^0.8.16; - -error RentrantCall(); - -/** - * @title ReentrancyGuard - * @notice Smart contract used to guard against potential reentrancy exploits. - * @dev Contract module that helps prevent reentrant calls to a function. - * - * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier - * available, which can be applied to functions to make sure there are no nested - * (reentrant) calls to them. - * - * Note that because there is a single `nonReentrant` guard, functions marked as - * `nonReentrant` may not call one another. This can be worked around by making - * those functions `private`, and then adding `external` `nonReentrant` entry - * points to them. - * - * TIP: If you would like to learn more about reentrancy and alternative ways - * to protect against it, check out our blog post - * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. - */ -abstract contract ReentrancyGuard { - // Booleans are more expensive than uint256 or any type that takes up a full - // word because each write operation emits an extra SLOAD to first read the - // slot's contents, replace the bits taken up by the boolean, and then write - // back. This is the compiler's defense against contract upgrades and - // pointer aliasing, and it cannot be disabled. - - // The values being non-zero value makes deployment a bit more expensive, - // but in exchange the refund on every call to nonReentrant will be lower in - // amount. Since refunds are capped to a percentage of the total - // transaction's gas, it is best to keep them low in cases like this one, to - // increase the likelihood of the full refund coming into effect. - uint256 private constant _NOT_ENTERED = 1; - uint256 private constant _ENTERED = 2; - - uint256 private _status; - - /** - * @notice Initializes the ReentrancyGuard with the `_status` of `_NOT_ENTERED`. - */ - constructor() { - _status = _NOT_ENTERED; - } - - /** - * @notice Used to ensure that the function it is applied to cannot be reentered. - * @dev Prevents a contract from calling itself, directly or indirectly. - * Calling a `nonReentrant` function from another `nonReentrant` - * function is not supported. It is possible to prevent this from happening - * by making the `nonReentrant` function external, and making it call a - * `private` function that does the actual work. - */ - modifier nonReentrant() { - _nonReentrantIn(); - _; - // By storing the original value once again, a refund is triggered (see - // https://eips.ethereum.org/EIPS/eip-2200) - _status = _NOT_ENTERED; - } - - /** - * @notice Used to ensure that the current call is not a reentrant call. - * @dev If reentrant call is detected, the execution will be reverted. - */ - function _nonReentrantIn() private { - // On the first call to nonReentrant, _notEntered will be true - if (_status == _ENTERED) revert RentrantCall(); - - // Any calls to nonReentrant after this point will fail - _status = _ENTERED; - } -} From f6a82e5e19fcf563db0dd3b8920d2c0335442892 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Wed, 25 Jan 2023 12:50:30 -0500 Subject: [PATCH 205/274] Update CI: Remove consensus and review labels as being exempt from stale (#6370) --- .github/workflows/stale.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f0eebe2a6c43e0..731f68b5313662 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,13 +24,12 @@ 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, e-consensus + exempt-issue-labels: discussions-to 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 From d005d7cacac96fe417208d624c2529902e9e7522 Mon Sep 17 00:00:00 2001 From: Pandapip1 <45835846+Pandapip1@users.noreply.github.com> Date: Wed, 25 Jan 2023 13:02:52 -0500 Subject: [PATCH 206/274] Update EIP template: Add acceptable placeholders (#5576) * Update EIP template: Remove non-required fields and add acceptable placeholders * Update eip-template.md * Update eip-template.md Co-authored-by: xinbenlv * Update eip-template.md * Add Test Cases and Reference Implementation sections back * Add Motivation section back * Apply suggestions from code review Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> * Small tweaks Co-authored-by: xinbenlv Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> Co-authored-by: Sam Wilson --- eip-template.md | 94 +++++++++++++++++++++++++++++++++++++++++-------- index.html | 2 +- 2 files changed, 80 insertions(+), 16 deletions(-) diff --git a/eip-template.md b/eip-template.md index d71731fb7b83aa..892fda11e785ca 100644 --- a/eip-template.md +++ b/eip-template.md @@ -1,55 +1,119 @@ --- -eip: title: description: author:
, FirstName (@GitHubUsername) and GitHubUsername (@GitHubUsername)> discussions-to: status: Draft type: -category (*only required for Standards Track): +category: # Only required for Standards Track. Otherwise, remove this field. created: -requires (*optional): +requires: # Only required when you reference an EIP in the `Specification` section. Otherwise, remove this field. --- -This is the suggested template for new EIPs. + ## 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", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. + + +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. ## 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. + + +TBD ## 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. + + +No backward compatibility issues found. ## 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. + + +Needs discussion. ## Copyright diff --git a/index.html b/index.html index 8ef51c6686e5ee..7f7c55bae4922c 100644 --- a/index.html +++ b/index.html @@ -12,7 +12,7 @@

EIPs

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.

Contributing

-

First review EIP-1. Then clone the repository and add your EIP to it. There is a template EIP here. Then submit a Pull Request to Ethereum's EIPs repository.

+

First review EIP-1. Then clone the repository and add your EIP to it. There is a template EIP here. Then submit a Pull Request to Ethereum's EIPs repository.

EIP status terms