diff --git a/.github/workflows/auto-review-trigger.yml b/.github/workflows/auto-review-trigger.yml index d11fb61ac6..5d3233f47d 100644 --- a/.github/workflows/auto-review-trigger.yml +++ b/.github/workflows/auto-review-trigger.yml @@ -56,7 +56,7 @@ jobs: files: pr-number.txt - name: Save PR Number - uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 if: steps.check_pr_number_exists.outputs.files_exists == 'true' with: name: pr-number diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bda74a5223..5ddd235fb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: echo $MERGE_SHA > ./pr/merge_sha - name: Upload PR Number - uses: actions/upload-artifact@65d862660abb392b8c4a3d1195a2108db131dd05 + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 with: name: pr_number path: pr/ @@ -42,12 +42,12 @@ jobs: steps: - name: Checkout EIPs - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: ethereum/EIPs path: '' - name: Checkout ERCs - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: repository: ethereum/ERCs path: ERCs diff --git a/ERCS/erc-7627.md b/ERCS/erc-7627.md index f9c6428703..0b986cd5cd 100644 --- a/ERCS/erc-7627.md +++ b/ERCS/erc-7627.md @@ -4,7 +4,8 @@ title: Secure Messaging Protocol description: End-to-end encryption for sending messages between users. author: Chen Liaoyuan (@chenly) discussions-to: https://ethereum-magicians.org/t/erc-7627-secure-messaging-protocol/18761 -status: Review +status: Last Call +last-call-deadline: 2025-02-18 type: Standards Track category: ERC created: 2024-02-19 diff --git a/ERCS/erc-7743.md b/ERCS/erc-7743.md index 992b5349bc..aebf738435 100755 --- a/ERCS/erc-7743.md +++ b/ERCS/erc-7743.md @@ -4,7 +4,7 @@ title: Multi-Owner Non-Fungible Tokens (MO-NFT) description: A new type of non-fungible token that supports multiple owners, allowing shared ownership and transferability among users. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/discussion-on-eip-7743-multi-owner-non-fungible-tokens-mo-nft/20577 -status: Draft +status: Review type: Standards Track category: ERC created: 2024-07-13 @@ -20,32 +20,6 @@ Traditional NFTs enforce a single-ownership model, which does not align with the ## Specification -### Data Structures - -- **Owners Mapping**: A mapping from `tokenId` to an enumerable set of owner addresses. - - ```solidity - mapping(uint256 => EnumerableSet.AddressSet) internal _owners; - ``` - -- **Balances Mapping**: A mapping from owner addresses to the number of tokens they own. - - ```solidity - mapping(address => uint256) private _balances; - ``` - -- **Providers Mapping**: A mapping from `tokenId` to the provider's address. - - ```solidity - mapping(uint256 => address) private _providers; - ``` - -- **Transfer Values Mapping**: A mapping from `tokenId` to the `transferValue` fee. - - ```solidity - mapping(uint256 => uint256) private _transferValues; - ``` - ### Token Creation and Ownership Model 1. **Minting**: @@ -80,15 +54,11 @@ Traditional NFTs enforce a single-ownership model, which does not align with the - Transferring ownership adds the new owner to the ownership list without removing current owners. This approach reflects the shared nature of digital assets. -2. **Ownership Tracking**: - - - The smart contract tracks the list of owners for each MO-NFT using the `_owners` mapping. - -3. **Provider Fee Handling**: +2. **Provider Fee Handling**: - During a transfer, the specified `transferValue` fee is transferred to the provider. The contract must have sufficient balance to cover this fee. -4. **Burning Ownership**: +3. **Burning Ownership**: - Owners can remove themselves from the ownership list using the `burn` function. @@ -96,16 +66,6 @@ Traditional NFTs enforce a single-ownership model, which does not align with the function burn(uint256 tokenId) external; ``` -### Value Depreciation - -1. **Value Model**: - - - As the number of owners increases, the value of the MO-NFT may decrease to reflect the reduced exclusivity. This is an optional model and can be implemented at the application level. - -2. **Depreciation Mechanism**: - - - The value depreciation model can be defined based on the asset type and distribution strategy. This standard does not enforce a specific depreciation mechanism but acknowledges its importance. - ### Interface Definitions **Minting Functions** @@ -152,7 +112,7 @@ Traditional NFTs enforce a single-ownership model, which does not align with the ### [ERC-721](./eip-721.md) Compliance -The MO-NFT standard is designed to be compatible with the [ERC-721](./eip-721.md) standard. It implements required functions such as `balanceOf`, `ownerOf`, and `transferFrom` from the IERC-721 interface. +The MO-NFT standard is designed to be compatible with the [ERC-721](./eip-721.md) standard. It implements required functions such as `balanceOf`, `ownerOf`, and `transferFrom` from the `ERC721` interface. - **Approval Functions**: Functions like `approve`, `getApproved`, `setApprovalForAll`, and `isApprovedForAll` are intentionally disabled or overridden, as they do not align with the MO-NFT multi-owner model. diff --git a/ERCS/erc-7750.md b/ERCS/erc-7750.md index 670ce525d1..9ccd321da2 100644 --- a/ERCS/erc-7750.md +++ b/ERCS/erc-7750.md @@ -4,7 +4,7 @@ title: Decentralized Employment System description: An employment system that records employment history. author: James Savechives (@jamesavechives) discussions-to: https://ethereum-magicians.org/t/erc-7750-decentralized-employment-system-des/20724 -status: Draft +status: Review type: Standards Track category: ERC created: 2024-08-04 @@ -290,39 +290,161 @@ interface IDecentralizedEmploymentSystem { ## Test Cases -1. **Company Creation**: - - **Input**: A user calls the `registerCompany("TechCorp", "Technology")` function. - - **Expected Output**: A new company is registered with a unique `companyId`, and the `companies` mapping is updated with the company's details (name: "TechCorp", industry: "Technology", owner: caller's address). - -2. **Employee Token Minting**: - - **Input**: The system owner calls `mintEmployeeToken(employeeAddress, "ipfs://metadataURI")`. - - **Expected Output**: A new SBT is minted for the employee with a unique `tokenId`, and the `employees` mapping is updated accordingly. - -3. **Contract Creation and Execution**: - - **Input**: - 1. A company with `companyId` `1` calls `createContract(1, 5, 1000, 6, "Software Development", "Failure to meet deadlines")`. - 2. Both the company and the employee sign the contract by calling `executeContract(contractId)`. - - **Expected Output**: - 1. A new labor contract is created with a unique `contractId`, and relevant mappings are updated. - 2. The contract status is set to "active" upon execution by both parties. - -4. **Salary Deposit**: - - **Input**: The company calls `depositSalary(contractId)` with a value of `1000 USDC`. - - **Expected Output**: The `escrowBalances` mapping is updated to reflect the deposited amount for `contractId`, securing the funds in escrow. - -5. **Salary Payment**: - - **Input**: After the contract duration, `releaseSalary(contractId)` is called. - - **Expected Output**: The escrowed `1000 USDC` is transferred to the employee's address, and the `escrowBalances` mapping for `contractId` is reset to zero. - -6. **Employment Termination**: - - **Input**: The company calls `terminateContract(contractId, "Failure to meet deadlines")`. - - **Expected Output**: - 1. The contract status is updated to "terminated" in the `contracts` mapping. - 2. A termination event is emitted, and the company is no longer obligated to continue salary payments. - -7. **Dispute Resolution**: - - **Input**: Either party calls `raiseDispute(contractId)`, followed by the assigned moderator calling `resolveDispute(contractId, true)` to favor the employee. - - **Expected Output**: The escrow funds are transferred to the employee, and the dispute is marked as resolved in the contract's status. +1. **Company Creation** + + **Input** + - A user calls `registerCompany("TechCorp", "Technology")`. + + **Expected State Changes** + - A new `companyId` is generated (e.g., `companyId = 1`). + - The `companies` mapping is updated: + ```solidity + companies[1]↦{ + name="TechCorp", + industry="Technology", + owner=callerAddress, + employeeIds=[ ] + } + ``` + - An event `CompanyRegistered` is emitted with the arguments `(1, callerAddress, "TechCorp", "Technology")`. + + **Expected Output** + - **Return Value**: `companyId = 1` (the newly created company ID). + - **Event**: `CompanyRegistered` is logged. + +2. **Employee Token Minting** + + **Input** + - The contract owner (or an authorized address) calls `mintEmployeeToken(employeeAddress, "ipfs://metadataURI")`. + + **Expected State Changes** + - A new token ID is generated (e.g., `tokenId = 5`). + - An internal mapping (e.g., `employeeTokenToOwner`) is updated: + ```solidity + employeeTokenToOwner[5]↦employeeAddress + ``` + - (Optional) If the implementation tracks metadata, another mapping (e.g., `employeeTokenMetadata`) might store: + ```solidity + employeeTokenMetadata[5]↦"ipfs://metadataURI" + ``` + - An event `EmployeeTokenMinted` is emitted with `(5, employeeAddress)`. + + **Expected Output** + - **Return Value**: `tokenId = 5` (the newly minted employee token ID). + - **Event**: `EmployeeTokenMinted` is logged. + +3. **Contract Creation and Execution** + + **Input** + 1. A company with `companyId = 1` calls: + ```solidity + createContract(1,5,1000,6,"SoftwareDevelopment","Failuretomeetdeadlines") + ``` + which returns `contractId`. + 2. Both the company and the employee call `executeContract(contractId)`. + + **Expected State Changes** + - **Contract Creation**: + 1. A new labor contract ID is generated, e.g., `contractId = 10`. + 2. The `contracts` mapping is updated: + ```solidity + contracts[10]↦{ + companyId=1, + employeeTokenId=5, + salary=1000, + duration=6, + responsibilities="SoftwareDevelopment", + terminationConditions="Failuretomeetdeadlines",status="Created" + } + ``` + 3. The system may also update a per-company or per-employee tracking structure (optional but typical): + ```solidity + companyContracts[1].push(10) + employeeContracts[5].push(10) + ``` + 4. An event `ContractCreated` is emitted with arguments `(10, 1, 5, 1000, 6)`. + - **Contract Execution**: + 1. Upon calls from both parties, the contract’s status changes from `"Created"` to `"Active"`: + ```solidity + contracts[10].status↦"Active" + ``` + 2. An event `ContractExecuted` is emitted with `(10)` once both signatures/confirmations are received. + + **Expected Output** + - **Return Value** (from `createContract`): `contractId = 10` + - **Event**: `ContractCreated(10, 1, 5, 1000, 6)` upon creation. + - **Event**: `ContractExecuted(10)` once execution is confirmed by both parties. + +4. **Salary Deposit** + + **Input** + - The company (owner of `companyId = 1`) calls `depositSalary(10)` and sends `1000 USDC` (or equivalent in wei for an [ERC-20](./eip-20.md) token or native token) to the contract. + + **Expected State Changes** + 1. The contract’s escrow balance mapping is updated: + ```solidity + escrowBalances[10]↦1000 + ``` + 2. An event `SalaryDeposited` is emitted with `(10, 1000)`. + + **Expected Output** + - **Event**: `SalaryDeposited(10, 1000)` + - The contract’s internal `escrowBalances[10]` should now be `1000`. + +5. **Salary Payment** + + **Input** + - After the contract’s duration or satisfaction of any release condition, `releaseSalary(10)` is called (by the contract or the employee). + + **Expected State Changes** + 1. The escrow balance for `contractId = 10` is transferred to the employee token owner (`employeeAddress` associated with token ID `5`). + 2. The `escrowBalances[10]` is set to `0`: + ```solidity + escrowBalances[10]↦0 + ``` + 3. An event `SalaryReleased` is emitted with `(10, employeeAddress)`. + + **Expected Output** + - **Event**: `SalaryReleased(10, employeeAddress)` + - The updated `escrowBalances[10]` is now `0`. + - The employee’s on-chain balance (or token balance if using [ERC-20](./eip-20.md)) increases by `1000`. + +6. **Employment Termination** + + **Input** + - The company calls `terminateContract(10, "Failure to meet deadlines")`. + + **Expected State Changes** + 1. The `contracts[10].status` is updated to `"Terminated"`: + ```solidity + contracts[10].status↦"Terminated" + ``` + 2. An event `ContractTerminated` is emitted with `(10, "Failure to meet deadlines")`. + + **Expected Output** + - **Event**: `ContractTerminated(10, "Failure to meet deadlines")` + - The `contracts[10]` status is now `"Terminated"`. + - No further salary obligations exist unless otherwise specified in dispute-resolution processes. + +7. **Dispute Resolution** + + **Input** + 1. Either party (company or employee) calls `raiseDispute(10)`. + 2. The assigned moderator calls `resolveDispute(10, true)` indicating the decision favors the employee. + + **Expected State Changes** + - **Dispute Raised**: + 1. The contract’s dispute status is noted (implementation-specific, but typically `contracts[10].disputeRaised = true`). + 2. An event `DisputeRaised(10, msg.sender)` is emitted. + - **Dispute Resolved**: + 1. If `decisionForEmployee == true`, any remaining escrow funds for `contractId = 10` are transferred to the employee. + 2. A `DisputeResolved(10, true)` event is emitted. + + **Expected Output** + - **Event**: `DisputeRaised(10, msg.sender)` + - **Event**: `DisputeResolved(10, true)` + - If funds remain in escrow, `escrowBalances[10]` is set to `0`, and the employee receives the outstanding balance. + ## Security Considerations diff --git a/ERCS/erc-7812.md b/ERCS/erc-7812.md new file mode 100644 index 0000000000..cd74096ca3 --- /dev/null +++ b/ERCS/erc-7812.md @@ -0,0 +1,522 @@ +--- +eip: 7812 +title: ZK Identity Registry +description: Singleton registry system for storing abstract private provable statements. +author: Artem Chystiakov (@arvolear) , Oleksandr Kurbatov , Yaroslav Panasenko , Michael Elliot (@michaelelliot) , Vitalik Buterin (@vbuterin) +discussions-to: https://ethereum-magicians.org/t/erc-7812-zk-identity-registry/21624 +status: Draft +type: Standards Track +category: ERC +created: 2024-11-08 +--- + +## Abstract + +This EIP introduces an on-chain registry system for storing and proving abstract statements. Users may utilize the system to store commitments to their private data to later prove its validity and authenticity via zero knowledge, without disclosing anything about the data itself. Moreover, developers may use the singleton `EvidenceRegistry` contract available at `0x` to integrate custom business-specific registrars for managing and processing particular statements. + +## Motivation + +This EIP stemmed from the need to localize and unravel the storage and issuance of provable statements so that future protocols can anchor to the standardized singleton on-chain registry and benefit from cross-reuse. + +The aggregation of provable statements significantly improves reusability, portability, and security of the abundance of zero knowledge privacy-oriented solutions. The abstract specification of the registry allows custom indentity-based, reputation-based, proof-of-attendance-based, etc., protocols to be implemented with little to minimal constraints. + +The given proposal lays the important foundation for specific solution to build upon. The more concrete specifications of statements and commitments structures are expected to emerge as separate, standalone EIPs. + +## 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. + +### Definitions + +- A "Sparse Merkle Tree (SMT)" is a special Merkle tree that works by deterministically and idempotently storing key/value pairs in the given locations leveraging a hash function. The Poseidon hash function is often used to optimize the compatibility with ZK. +- A "statement" is an accepted structured representation of some abstract evidence. A statement can range from a simple `string` to a Merkle root of some SMT. +- A "commitment" is a special public value resulting from blinding a statement to conceal it. Commitments allow the authenticity of a statement to be proven in ZK without disclosing the statement itself. +- A "commitment key" is a private salt mixed with the statement to obtain a commitment to that statement. The commitment key must be kept private to maintain the confidentiality of statements. + +### General + +The on-chain registry system consists of two subsystems: the `EvidenceRegistry` with `EvidenceDB` and `Registrar` components. This EIP will focus on describing and standardizing the former, while the `Registrar` specification may be amended as the separate proposals. + +![The on-chain evidence registry system entities diagram.](../assets/eip-7812/images/diagram.png) + +The on-chain evidence registry system entities diagram. + +The `EvidenceRegistry` acts as the entrypoint to a protocol-wide provable database `EvidenceDB` where arbitrary `32-byte` data can be written to and later proven on demand. The `Registrar` entities implement specific business use cases, structure the provable data, and utilize `EvidenceRegistry` to put this data in the `EvidenceDB`. + +In order to prove that certain data is or is not present in the `EvidenceDB` Merkle proofs may be used. Understanding how a specific `Registrar` has structured and put data into the `EvidenceDB`, one may implement an on-chain ZK verifier (using Circom or any other stack) and prove the inclusion (or exclusion) of the data in the database. + +The Circom implementation of a general-purpose SMT-driven `EvidenceDB` verifier circuit together with the Solidity implementation of `EvidenceRegistry` and `EvidenceDB` smart contracts may be found in the "Reference Implementation" section. + +### Evidence DB + +The `EvidenceDB` smart contract MAY implement an arbitrary provable key/value data structure, however it MUST support the `addition`, `update`, and `removal` of elements. All of the supported write operations MUST maintain the property of idempotence (e.i. `addition` followed by `removal` should not change the state of the database). The data structure of choice MUST be capable of providing both element inclusion and exclusion proofs. The functions that modify the `EvidenceDB` state MUST be callable only by the `EvidenceRegistry`. + +For reference, the `EvidenceDB` smart contract MAY implement the following interface: + +```solidity +pragma solidity ^0.8.0; + +/** + * @notice Evidence DB interface for Sparse Merkle Tree based statements database. + */ +interface IEvidenceDB { + /** + * @notice Represents the proof of a node's inclusion/exclusion in the tree. + * @param root The root hash of the Merkle tree. + * @param siblings An array of sibling hashes can be used to get the Merkle Root. + * @param existence Indicates the presence (true) or absence (false) of the node. + * @param key The key associated with the node. + * @param value The value associated with the node. + * @param auxExistence Indicates the presence (true) or absence (false) of an auxiliary node. + * @param auxKey The key of the auxiliary node. + * @param auxValue The value of the auxiliary node. + */ + struct Proof { + bytes32 root; + bytes32[] siblings; + bool existence; + bytes32 key; + bytes32 value; + bool auxExistence; + bytes32 auxKey; + bytes32 auxValue; + } + + /** + * @notice Adds the new element to the tree. + */ + function add(bytes32 key, bytes32 value) external; + + /** + * @notice Removes the element from the tree. + */ + function remove(bytes32 key) external; + + /** + * @notice Updates the element in the tree. + */ + function update(bytes32 key, bytes32 newValue) external; + + /** + * @notice Gets the SMT root. + * SHOULD NOT be used on-chain due to roots frontrunning. + */ + function getRoot() external view returns (bytes32); + + /** + * @notice Gets the number of nodes in the tree. + */ + function getSize() external view returns (uint256); + + /** + * @notice Gets the max tree height (number of branches in the Merkle proof) + */ + function getMaxHeight() external view returns (uint256); + + /** + * @notice Gets Merkle inclusion/exclusion proof of the element. + */ + function getProof(bytes32 key) external view returns (Proof memory); + + /** + * @notice Gets the element value by its key. + */ + function getValue(bytes32 key) external view returns (bytes32); +} +``` + +### Evidence Registry + +The `EvidenceRegistry` smart contract is the central piece of this EIP. The `EvidenceRegistry` MUST implement the following interface, however, it MAY be extended: + +```solidity +pragma solidity ^0.8.0; + +/** + * @notice Common Evidence Registry interface. + */ +interface IEvidenceRegistry { + /** + * @notice MUST be emitted whenever the Merkle root is updated. + */ + event RootUpdated(bytes32 indexed prev, bytes32 indexed curr); + + /** + * @notice Adds the new statement to the DB. + */ + function addStatement(bytes32 key, bytes32 value) external; + + /** + * @notice Removes the statement from the DB. + */ + function removeStatement(bytes32 key) external; + + /** + * @notice Updates the statement in the DB. + */ + function updateStatement(bytes32 key, bytes32 newValue) external; + + /** + * @notice Retrieves historical DB roots creation timestamps. + * Latest root MUST return `block.timestamp`. + * Non-existent root MUST return `0`. + */ + function getRootTimestamp(bytes32 root) external view returns (uint256); +} +``` + +The `addStatement`, `removeStatement`, and `updateStatement` methods MUST isolate the statement `key` in order for the database to allocate a specific namespace for a caller. These methods MUST revert in case the isolated key being added already exists in the `EvidenceDB` or the isolated key being removed or updated does not. + +The `EvidenceRegistry` MUST maintain the linear history of `EvidenceDB` roots. The `getRootTimestamp` method MUST NOT revert. Instead, it MUST return `0` in case the queried `root` does not exist. The method MUST return `block.timestamp` in case the latest root is requested. + +Before communicating with the `EvidenceDB`, the `key` MUST be isolated in the following way: + +```solidity +bytes32 isolatedKey = hash(msg.sender, key) +``` + +Where the `hash` is secure protocol-wide hash function of choice. + +### Hash Function + +The same secure hash function MUST be employed in both `EvidenceRegistry` and `EvidenceDB`. It is RECOMMENDED to use ZK-friendly hash function such as `poseidon` to streamline the database proving. + +In case ZK-friendly hash function is chosen, `EvidenceRegistry` MUST NOT accept `keys` or `values` beyond the underlying elliptic curve prime field size (`21888242871839275222246405745257275088548364400416034343698204186575808495617` for `BN128`). + +## Rationale + +During the EIP specification we have considered two approaches: where every protocol has its own registry and where all protocols are united under a singleton registry. We have decided to go with the latter as this approach provides the following benefits: + +1. Cross-chain portability. Only a single `bytes32` value (the SMT root) has to be sent cross-chain to be able to prove the state of the registry. +2. Centralization of trust. Users only need to trust a single, permissionaless, immutable smart contract. +3. Integration streamline. The singleton design formalizes the system interface, the hash function, and the overall proofs structure to simplify the integration. + +The proposal is deliberately written as abstract as possible to not constrain the possible business use cases and allow `Registrars` to implement arbitrary provable solutions. + +It is expected that based on this work future EIPs will describe concrete registrars with the exact procedures of generation of commitments, management of commitment keys, and proving of operated statements. For instance, there may be a registrar for on-chain accounting of national passports, a registrar with [EIP-4337](./eip-4337.md) confidential account identity management, a registrar for POAPs, etc. + +The `EvidenceDB` namespacing is chosen to segregate the write access to the database cells, ensuring that no entity but issuer can alter their content. However, this decision delegates the access control management responsibility solely to registrars, an important aspect to be considered during their development. + +The `EvidenceRegistry` maintains the minimal viable (gas-wise) history of roots on-chain for smooth registrars integration. In case more elaborate history is required, it is RECOMMENDED to implement off-chain services for parsing of `RootUpdated` events. + +## Backwards Compatibility + +This EIP is fully backwards compatible. + +### Deployment Method + +The `EvidenceRegistry` is a singleton contract available at `0x` deployed via the "deterministic deployment proxy" from `0x4e59b44847b379578588920ca78fbf26c0b4956c` with the salt `0x` . + +## Reference Implementation + +The reference implementation of `EvidenceRegistry` and `EvidenceDB` Solidity smart contracts together with the evidence registry state verifier Circom circuit is provided in the proposal. + +The low-level Solidity and Circom implementations of SMT can be found [here](../assets/eip-7812/contracts/SparseMerkleTree.sol) and [here](../assets/eip-7812/circuits/SparseMerkleTree.circom). + +The height of the SMT is set to `80`. + +> Please note that the reference implementation depends on the `@openzeppelin/contracts v5.1.0` and `circomlib v2.0.5`. + +### EvidenceDB Implementation + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol"; + +import {SparseMerkleTree} from "./libraries/SparseMerkleTree.sol"; +import {PoseidonUnit2L, PoseidonUnit3L} from "./libraries/Poseidon.sol"; + +contract EvidenceDB is IEvidenceDB, Initializable { + using SparseMerkleTree for SparseMerkleTree.SMT; + + address private _evidenceRegistry; + + SparseMerkleTree.SMT private _tree; + + modifier onlyEvidenceRegistry() { + _requireEvidenceRegistry(); + _; + } + + function __EvidenceDB_init(address evidenceRegistry_, uint32 maxDepth_) external initializer { + _evidenceRegistry = evidenceRegistry_; + + _tree.initialize(maxDepth_); + + _tree.setHashers(_hash2, _hash3); + } + + /** + * @inheritdoc IEvidenceDB + */ + function add(bytes32 key_, bytes32 value_) external onlyEvidenceRegistry { + _tree.add(key_, value_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function remove(bytes32 key_) external onlyEvidenceRegistry { + _tree.remove(key_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function update(bytes32 key_, bytes32 newValue_) external onlyEvidenceRegistry { + _tree.update(key_, newValue_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getRoot() external view returns (bytes32) { + return _tree.getRoot(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getSize() external view returns (uint256) { + return _tree.getNodesCount(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getMaxHeight() external view returns (uint256) { + return _tree.getMaxDepth(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getProof(bytes32 key_) external view returns (Proof memory) { + return _tree.getProof(key_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getValue(bytes32 key_) external view returns (bytes32) { + return _tree.getNodeByKey(key_).value; + } + + /** + * @notice Returns the address of the Evidence Registry. + */ + function getEvidenceRegistry() external view returns (address) { + return _evidenceRegistry; + } + + function _requireEvidenceRegistry() private view { + if (_evidenceRegistry != msg.sender) { + revert NotFromEvidenceRegistry(msg.sender); + } + } + + function _hash2(bytes32 element1_, bytes32 element2_) private pure returns (bytes32) { + return PoseidonUnit2L.poseidon([element1_, element2_]); + } + + function _hash3( + bytes32 element1_, + bytes32 element2_, + bytes32 element3_ + ) private pure returns (bytes32) { + return PoseidonUnit3L.poseidon([element1_, element2_, element3_]); + } +} +``` + +### EvidenceRegistry Implementation + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol"; +import {IEvidenceRegistry} from "./interfaces/IEvidenceRegistry.sol"; + +import {PoseidonUnit2L} from "./libraries/Poseidon.sol"; + +contract EvidenceRegistry is IEvidenceRegistry, Initializable { + uint256 public constant BABY_JUB_JUB_PRIME_FIELD = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + IEvidenceDB private _evidenceDB; + + mapping(bytes32 => uint256) private _rootTimestamps; + + modifier onlyInPrimeField(bytes32 key) { + _requireInPrimeField(key); + _; + } + + modifier onRootUpdate() { + bytes32 prevRoot_ = _evidenceDB.getRoot(); + _rootTimestamps[prevRoot_] = block.timestamp; + _; + emit RootUpdated(prevRoot_, _evidenceDB.getRoot()); + } + + function __EvidenceRegistry_init(address evidenceDB_) external initializer { + _evidenceDB = IEvidenceDB(evidenceDB_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function addStatement( + bytes32 key_, + bytes32 value_ + ) external onlyInPrimeField(key_) onlyInPrimeField(value_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) != bytes32(0)) { + revert KeyAlreadyExists(key_); + } + + _evidenceDB.add(isolatedKey_, value_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function removeStatement(bytes32 key_) external onlyInPrimeField(key_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) { + revert KeyDoesNotExist(key_); + } + + _evidenceDB.remove(isolatedKey_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function updateStatement( + bytes32 key_, + bytes32 newValue_ + ) external onlyInPrimeField(key_) onlyInPrimeField(newValue_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) { + revert KeyDoesNotExist(key_); + } + + _evidenceDB.update(isolatedKey_, newValue_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function getRootTimestamp(bytes32 root_) external view returns (uint256) { + if (root_ == bytes32(0)) { + return 0; + } + + if (root_ == _evidenceDB.getRoot()) { + return block.timestamp; + } + + return _rootTimestamps[root_]; + } + + function getEvidenceDB() external view returns (address) { + return address(_evidenceDB); + } + + function _getIsolatedKey(bytes32 key_) internal view returns (bytes32) { + return PoseidonUnit2L.poseidon([bytes32(uint256(uint160(msg.sender))), key_]); + } + + function _requireInPrimeField(bytes32 key_) private pure { + if (uint256(key_) >= BABY_JUB_JUB_PRIME_FIELD) { + revert NumberNotInPrimeField(key_); + } + } +} +``` + +### EvidenceRegistry Verifier Implementation + +```solidity +// LICENSE: CC0-1.0 +pragma circom 2.1.9; + +include "SparseMerkleTree.circom"; + +template BuildIsolatedKey() { + signal output isolatedKey; + + signal input address; + signal input key; + + component hasher = Poseidon(2); + hasher.inputs[0] <== address; + hasher.inputs[1] <== key; + + hasher.out ==> isolatedKey; +} + +template EvidenceRegistrySMT(levels) { + // Public Inputs + signal input root; + + // Private Inputs + signal input address; + signal input key; + + signal input value; + + signal input siblings[levels]; + + signal input auxKey; + signal input auxValue; + signal input auxIsEmpty; + + signal input isExclusion; + + // Build isolated key + component isolatedKey = BuildIsolatedKey(); + isolatedKey.address <== address; + isolatedKey.key <== key; + + // Verify Sparse Merkle Tree Proof + component smtVerifier = SparseMerkleTree(levels); + smtVerifier.siblings <== siblings; + + smtVerifier.key <== isolatedKey.isolatedKey; + smtVerifier.value <== value; + + smtVerifier.auxKey <== auxKey; + smtVerifier.auxValue <== auxValue; + smtVerifier.auxIsEmpty <== auxIsEmpty; + + smtVerifier.isExclusion <== isExclusion; + + smtVerifier.root <== root; +} + +component main {public [root]} = EvidenceRegistrySMT(80); +``` + +## Security Considerations + +From security standpoint there are several important aspects that must be highlighted. + +The individual registrars are expected to provide the functionality for both management and proving of statements. The proving will often be carried out by ZK proofs, which require trusted setup. Improperly setup ZK verifiers can be exploited to verify forged proofs. + +The `getRoot` method of `EvidenceDB` SHOULD NOT be used on-chain by the integrating registrars to check the validity of the database state. Instead, the required `root` SHOULD be passed as a function parameter and checked via `getRootTimestamp` method to avoid being frontrun. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/ERCS/erc-7837.md b/ERCS/erc-7837.md new file mode 100755 index 0000000000..533257c023 --- /dev/null +++ b/ERCS/erc-7837.md @@ -0,0 +1,407 @@ +--- +eip: 7837 +title: Diffusive Tokens +description: A fungible token that mints new tokens on transfer, charges a per-token native fee, and enforces a capped supply. +author: James Savechives (@jamesavechives) +discussions-to: https://ethereum-magicians.org/t/erc-7837-diffusive-tokens/21989 +status: Draft +type: Standards Track +category: ERC +created: 2024-12-07 +--- + +## Abstract + +This ERC proposes a standard for a new type of fungible token, called **Diffusive Tokens (DIFF)**. Unlike traditional [ERC-20](./eip-20.md) tokens, transferring DIFF tokens does not decrease the sender’s balance. Instead, it *mints* new tokens directly to the recipient, increasing the total supply on every transfer action. A fixed native currency fee is charged per token transferred, and this fee is paid by the sender to the contract owner. The supply growth is limited by a maximum supply set by the owner. Token holders can also burn their tokens to reduce the total supply. These features enable a controlled, incentivized token distribution model that merges fungibility with a built-in economic mechanism. + +## Motivation + +Traditional [ERC-20](./eip-20.md) tokens maintain a constant total supply and simply redistribute balances on transfers. While this model is widespread, certain use cases benefit from a token design that continuously expands supply during transfers, simulating a controlled "diffusion" of value. The Diffusive Token model may be suitable for representing claims on real-world goods (e.g., a product batch like iPhone 15 units), digital goods, or controlled asset distributions where initial token distribution and ongoing availability need to be managed differently. + +This model also includes a native currency fee per token transferred, incentivizing careful, value-driven transfers and providing a revenue stream for the token’s issuer. The maximum supply cap prevents unbounded inflation, ensuring long-term scarcity. The ability for owners to burn tokens to redeem underlying goods or services directly maps on-chain assets to real-world redemptions. + +**Use Cases**: + +- **Real-World Asset Backing**: A manufacturer can issue DIFF tokens representing a batch of products (e.g., iPhones). Each token can be redeemed (burned) for one physical item. + +- **Fee-Driven Incentives**: The transfer fee ensures that infinite minting by constant transferring is economically disincentivized. The fee also supports the token issuer or provides a funding mechanism. + + +## Specification + +### Terminology + +- **Diffusive Token**: A fungible token unit that is minted on transfers. +- **Max Supply**: The maximum total supply the token can reach. +- **Transfer Fee**: A fee in native blockchain currency (e.g., ETH) that must be paid by the sender for each token transferred. The total fee = `transferFee * amount`. +- **Burn**: The action of destroying tokens, reducing both the holder’s balance and the total supply. + +### Data Structures + +- **Total Supply and Max Supply**: + + ```solidity + uint256 public totalSupply; + uint256 public maxSupply; + ``` + +- **Transfer Fee**: + + ```solidity + uint256 public transferFee; // fee per token transferred in wei + address public owner; + ``` + + The `owner` sets and updates `transferFee` and `maxSupply`. + +### Token Semantics + +1. **Minting on Transfer** + When a transfer occurs from `A` to `B`: + - `A` does not lose any tokens. + - `B` receives newly minted tokens (increasing their balance and totalSupply). + - The `totalSupply` increases by the transferred amount, but must not exceed `maxSupply`. + +2. **Fixed Transfer Fee in Native Currency** + Each transfer requires the sender to pay `transferFee * amount` in the native currency. If `msg.value` is insufficient, the transaction reverts. + +3. **Maximum Supply** + If a transfer would cause `totalSupply + amount > maxSupply`, it must revert. + +4. **Burning Tokens** + Token holders can burn tokens to: + - Reduce their balance by the burned amount. + - Decrease `totalSupply` by the burned amount. + + This can map to redeeming underlying goods or simply deflating the token. + +### Interface + +The DIFF standard aligns partially with [ERC-20](./eip-20.md), but redefines certain behaviors: + +**Core Functions:** + +- `function balanceOf(address account) external view returns (uint256);` + +- `function transfer(address to, uint256 amount) external payable returns (bool);` + + - **Modified behavior**: Mints `amount` tokens to `to`, requires `msg.value >= transferFee * amount`. + +- `function burn(uint256 amount) external;` + + - Reduces sender’s balance and `totalSupply`. + +**Administration Functions (Owner Only):** + +- `function setMaxSupply(uint256 newMax) external;` + +- `function setTransferFee(uint256 newFee) external;` + +- `function withdrawFees(address payable recipient) external;` + + - Withdraws accumulated native currency fees. + +**Optional Approval Interface (For Compatibility):** + +- `function approve(address spender, uint256 amount) external returns (bool);` +- `function transferFrom(address from, address to, uint256 amount) external payable returns (bool);` + + - **Modified behavior**: Similar to `transfer`, but uses allowance and still mints tokens to `to` rather than redistributing from `from`. + +### Events + +- `event Transfer(address indexed from, address indexed to, uint256 amount);` + + Emitted when tokens are minted to `to` via a transfer call. + +- `event Burn(address indexed burner, uint256 amount);` + + Emitted when `amount` of tokens are burned from an address. + +- `event FeeUpdated(uint256 newFee);` + + Emitted when the owner updates the `transferFee`. + +- `event MaxSupplyUpdated(uint256 newMaxSupply);` + + Emitted when the owner updates `maxSupply`. + +### Compliance with ERC-20 + +The DIFF standard implements the ERC-20 interface but significantly alters the `transfer` and `transferFrom` semantics: + +- **Fungibility**: Each token unit is identical and divisible as in ERC-20. +- **Balances and Transfers**: The `balanceOf` function works as normal. However, `transfer` and `transferFrom` no longer redistribute tokens. Instead, they mint new tokens (up to `maxSupply`). +- **Approvals**: The `approve` and `transferFrom` functions remain, but their logic is unconventional since the sender’s balance is never reduced by transfers. + +While the DIFF standard can be seen as ERC-20 compatible at the interface level, the underlying economics differ substantially. + +## Rationale + +**Design Decisions**: + +- **Unlimited Minting vs. Max Supply**: Allowing minting on every transfer provides a “diffusive” spread of tokens. The `maxSupply` prevents uncontrolled inflation. + +- **Burn Mechanism**: Enables redemption or deflation as tokens are taken out of circulation. + +- **Owner Controls**: The owner (e.g., issuer) can adjust fees and max supply, maintaining flexibility as market conditions change. + +## Backwards Compatibility + +The DIFF standard is interface-compatible with ERC-20 but not behaviorally identical. Any system integrating DIFF tokens should understand the difference in minting on transfer. + +- **Wallets and Exchanges**: Most ERC-20 compatible tools can display balances and initiate transfers. However, the unusual economics (mint on transfer) may confuse users and pricing mechanisms. +- **Allowances and TransferFrom**: Still implemented for interoperability, but the expected logic (debiting `from` balance) does not apply. + +## Test Cases + +1. **Initial Conditions**: + - Deploy contract with `maxSupply = 1,000,000 DIFF`, `transferFee = 0.001 ETH`. + - `totalSupply = 0`. + - Owner sets parameters and verifies via `maxSupply()` and `transferFee()` getters. + +2. **Minting on Transfer**: + - User A calls `transfer(B, 100)` with `msg.value = 0.1 ETH` (assuming `transferFee = 0.001 ETH`). + - Check `balances[B] == 100`, `totalSupply == 100`. + - Check that the contract now holds 0.1 ETH from the fee. + +3. **Exceeding Max Supply**: + - If `totalSupply = 999,950` and someone tries to transfer 100 tokens, causing `totalSupply` to exceed `1,000,000`, the transaction reverts. + +4. **Burning Tokens**: + - User B calls `burn(50)`. + - Check `balances[B] == 50`, `totalSupply == 50` less than before. + - `Burn` event emitted. + +5. **Updating Fee and Withdrawing Funds**: + - Owner calls `setTransferFee(0.002 ETH)`. + - `FeeUpdated` event emitted. + - Owner calls `withdrawFees(ownerAddress)`. + - Check that `ownerAddress` receives accumulated fees. + +## Reference Implementation + +A reference implementation is provided under the asset folder in the EIPs repository. The implementation includes: + +- A basic contract implementing the DIFF standard. +```solidity +contract DiffusiveToken { + // ----------------------------------------- + // State Variables + // ----------------------------------------- + + string public name; + string public symbol; + uint8 public decimals; + + uint256 public totalSupply; + uint256 public maxSupply; + uint256 public transferFee; // Fee per token transferred in wei + + address public owner; + + // ----------------------------------------- + // Events + // ----------------------------------------- + + event Transfer(address indexed from, address indexed to, uint256 amount); + event Burn(address indexed burner, uint256 amount); + event FeeUpdated(uint256 newFee); + event MaxSupplyUpdated(uint256 newMaxSupply); + event Approval(address indexed owner, address indexed spender, uint256 value); + + // ----------------------------------------- + // Modifiers + // ----------------------------------------- + + modifier onlyOwner() { + require(msg.sender == owner, "DiffusiveToken: caller is not the owner"); + _; + } + + // ----------------------------------------- + // Constructor + // ----------------------------------------- + + /** + * @dev Constructor sets the initial parameters for the Diffusive Token. + * @param _name Token name + * @param _symbol Token symbol + * @param _decimals Decimal places + * @param _maxSupply The max supply of tokens that can ever exist + * @param _transferFee Initial fee per token transferred in wei + */ + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _maxSupply, + uint256 _transferFee + ) { + name = _name; + symbol = _symbol; + decimals = _decimals; + maxSupply = _maxSupply; + transferFee = _transferFee; + owner = msg.sender; + totalSupply = 0; // Initially, no tokens are minted + } + + // ----------------------------------------- + // External and Public Functions + // ----------------------------------------- + + /** + * @notice Returns the token balance of the given address. + * @param account The address to query + */ + function balanceOf(address account) external view returns (uint256) { + return balances[account]; + } + + /** + * @notice Transfers `amount` tokens to address `to`, minting new tokens in the process. + * @dev Requires payment of native currency: transferFee * amount. + * @param to Recipient address + * @param amount Number of tokens to transfer + * @return True if successful + */ + function transfer(address to, uint256 amount) external payable returns (bool) { + require(to != address(0), "DiffusiveToken: transfer to zero address"); + require(amount > 0, "DiffusiveToken: amount must be greater than zero"); + + uint256 requiredFee = transferFee * amount; + require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee"); + + // Check max supply limit + require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply"); + + // Mint new tokens to `to` + balances[to] += amount; + totalSupply += amount; + + emit Transfer(msg.sender, to, amount); + return true; + } + + /** + * @notice Burns `amount` tokens from the caller's balance, decreasing total supply. + * @param amount The number of tokens to burn + */ + function burn(uint256 amount) external { + require(amount > 0, "DiffusiveToken: burn amount must be greater than zero"); + require(balances[msg.sender] >= amount, "DiffusiveToken: insufficient balance"); + + balances[msg.sender] -= amount; + totalSupply -= amount; + + emit Burn(msg.sender, amount); + } + + /** + * @notice Approves `spender` to transfer up to `amount` tokens on behalf of `msg.sender`. + * @param spender The address authorized to spend + * @param amount The max amount they can spend + */ + function approve(address spender, uint256 amount) external returns (bool) { + require(spender != address(0), "DiffusiveToken: approve to zero address"); + allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + /** + * @notice Returns the current allowance of `spender` for `owner`. + * @param _owner The owner of the tokens + * @param _spender The address allowed to spend the tokens + */ + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + /** + * @notice Transfers `amount` tokens from `from` to `to` using the allowance mechanism. + * @dev The `from` account does not lose tokens; this still mints to `to`. + * @param from The address from which the allowance has been given + * @param to The recipient address + * @param amount The number of tokens to transfer (mint) + */ + function transferFrom(address from, address to, uint256 amount) external payable returns (bool) { + require(to != address(0), "DiffusiveToken: transfer to zero address"); + require(amount > 0, "DiffusiveToken: amount must be greater than zero"); + + uint256 allowed = allowances[from][msg.sender]; + require(allowed >= amount, "DiffusiveToken: allowance exceeded"); + + // Deduct from allowance + allowances[from][msg.sender] = allowed - amount; + + uint256 requiredFee = transferFee * amount; + require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee"); + + // Check max supply + require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply"); + + // Mint tokens to `to` + balances[to] += amount; + totalSupply += amount; + + emit Transfer(from, to, amount); + return true; + } + + // ----------------------------------------- + // Owner Functions + // ----------------------------------------- + + /** + * @notice Updates the maximum supply of tokens. Must be >= current totalSupply. + * @param newMaxSupply The new maximum supply + */ + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + require(newMaxSupply >= totalSupply, "DiffusiveToken: new max < current supply"); + maxSupply = newMaxSupply; + emit MaxSupplyUpdated(newMaxSupply); + } + + /** + * @notice Updates the per-token transfer fee. + * @param newFee The new fee in wei per token transferred + */ + function setTransferFee(uint256 newFee) external onlyOwner { + transferFee = newFee; + emit FeeUpdated(newFee); + } + + /** + * @notice Allows the owner to withdraw accumulated native currency fees. + * @param recipient The address that will receive the withdrawn fees + */ + function withdrawFees(address payable recipient) external onlyOwner { + require(recipient != address(0), "DiffusiveToken: withdraw to zero address"); + uint256 balance = address(this).balance; + (bool success, ) = recipient.call{value: balance}(""); + require(success, "DiffusiveToken: withdrawal failed"); + } + + // ----------------------------------------- + // Fallback and Receive + // ----------------------------------------- + + // Allows the contract to receive Ether. + receive() external payable {} +} +``` + +- Interfaces and helper contracts for testing and demonstration purposes. + +## Security Considerations + +- **Reentrancy**: Handle fee transfers using the Checks-Effects-Interactions pattern. Consider `ReentrancyGuard` from OpenZeppelin to prevent reentrant calls. +- **Overflow/Underflow**: Solidity 0.8.x guards against this by default. +- **Contract Balance Management**: Ensure enough native currency is sent to cover fees. Revert on insufficient fees. +- **Access Control**: Only the owner can update `transferFee` and `maxSupply`. Use proper `onlyOwner` modifiers. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7812/circuits/SparseMerkleTree.circom b/assets/erc-7812/circuits/SparseMerkleTree.circom new file mode 100644 index 0000000000..22231aa0b7 --- /dev/null +++ b/assets/erc-7812/circuits/SparseMerkleTree.circom @@ -0,0 +1,272 @@ +// LICENSE: MIT +pragma circom 2.1.9; + +include "circomlib/circuits/poseidon.circom"; +include "circomlib/circuits/switcher.circom"; +include "circomlib/circuits/gates.circom"; +include "circomlib/circuits/bitify.circom"; + +function inverse(a) { + return 1 - a; +} + +/* + * Hash2 = Poseidon(H_L | H_R) + */ +template Hash2() { + signal input a; + signal input b; + + signal output out; + + component h = Poseidon(2); + h.inputs[0] <== a; + h.inputs[1] <== b; + + out <== h.out; +} + +/* + * Hash2 = Poseidon(key | value | 1) + * 1 is added to the end of the leaf value to make the hash unique + */ +template Hash3() { + signal input a; + signal input b; + signal input c; + + signal output out; + + c === 1; + + component h = Poseidon(3); + h.inputs[0] <== a; + h.inputs[1] <== b; + h.inputs[2] <== c; + + out <== h.out; +} + +/* +* Returns an array of bits, where the index of `1` bit +* is the current depth of the tree +*/ +template DepthDeterminer(depth) { + assert(depth > 1); + + signal input siblings[depth]; + signal output desiredDepth[depth]; + + signal done[depth - 1]; + + component isZero[depth]; + + for (var i = 0; i < depth; i++) { + isZero[i] = IsZero(); + isZero[i].in <== siblings[i]; + } + + // The last sibling is always zero due to the way the proof is constructed + isZero[depth - 1].out === 1; + + // If there is a branch on the previous depth, then the current depth is the desired one + desiredDepth[depth - 1] <== inverse(isZero[depth - 2].out); + done[depth - 2] <== desiredDepth[depth - 1]; + + // desiredDepth will be `1` the first time we encounter non-zero branch on the previous depth + for (var i = depth - 2; i > 0; i--) { + desiredDepth[i] <== inverse(done[i]) * inverse(isZero[i - 1].out); + done[i - 1] <== desiredDepth[i] + done[i]; + } + + desiredDepth[0] <== inverse(done[0]); +} + +/* + * Determines the type of the node + */ +template NodeTypeDeterminer() { + signal input auxIsEmpty; + // 1 if the node is at the desired depth, 0 otherwise + signal input isDesiredDepth; + signal input isExclusion; + + signal input previousMiddle; + signal input previousEmpty; + signal input previousAuxLeaf; + signal input previousLeaf; + + // 1 if the node is a middle node, 0 otherwise + signal output middle; + // 1 if the node is an empty node, 0 otherwise + signal output empty; + // 1 if the node is a leaf node for the exclusion proof, 0 otherwise + signal output auxLeaf; + // 1 if the node is a leaf node, 0 otherwise + signal output leaf; + + // 1 if the node is a leaf node and we are checking for exclusion, 0 otherwise + signal leafForExclusionCheck <== isDesiredDepth * isExclusion; + + // Determine the node as a middle, until getting to the desired depth + middle <== previousMiddle - isDesiredDepth; + + // Determine the node as a leaf, when we are at the desired depth and + // we check for inclusion + leaf <== isDesiredDepth - leafForExclusionCheck; + + // Determine the node as an auxLeaf, when we are at the desired depth and + // we check for exclusion in a bamboo scenario + auxLeaf <== leafForExclusionCheck * inverse(auxIsEmpty); + + // Determine the node as an empty, when we are at the desired depth and + // we check for exclusion with an empty node + empty <== isDesiredDepth * auxIsEmpty; +} + +/* + * Gets hash at the current depth, based on the type of the node + * If the mode is a empty, then the hash is 0 + */ +template DepthHasher() { + signal input isMiddle; + signal input isAuxLeaf; + signal input isLeaf; + + signal input sibling; + signal input auxLeaf; + signal input leaf; + signal input currentKeyBit; + signal input child; + + signal output root; + + component switcher = Switcher(); + switcher.L <== child; + switcher.R <== sibling; + // Based on the current key bit, we understand which order to use + switcher.sel <== currentKeyBit; + + component proofHash = Hash2(); + proofHash.a <== switcher.outL; + proofHash.b <== switcher.outR; + + signal res[3]; + // hash of the middle node + res[0] <== proofHash.out * isMiddle; + // hash of the aux leaf node for the exclusion proof + res[1] <== auxLeaf * isAuxLeaf; + // hash of the leaf node for the inclusion proof + res[2] <== leaf * isLeaf; + + // only one of the following will be non-zero + root <== res[0] + res[1] + res[2]; +} + +/* + * Checks the sparse merkle proof against the given root + */ +template SparseMerkleTree(depth) { + // The root of the sparse merkle tree + signal input root; + // The siblings for each depth + signal input siblings[depth]; + + signal input key; + signal input value; + + signal input auxKey; + signal input auxValue; + // 1 if the aux node is empty, 0 otherwise + signal input auxIsEmpty; + + // 1 if we are checking for exclusion, 0 if we are checking for inclusion + signal input isExclusion; + + // Check that the auxIsEmpty is 0 if we are checking for inclusion + component exclusiveCase = AND(); + exclusiveCase.a <== inverse(isExclusion); + exclusiveCase.b <== auxIsEmpty; + exclusiveCase.out === 0; + + // Check that the key != auxKey if we are checking for exclusion and the auxIsEmpty is 0 + component areKeyEquals = IsEqual(); + areKeyEquals.in[0] <== auxKey; + areKeyEquals.in[1] <== key; + + component keysOk = MultiAND(3); + keysOk.in[0] <== isExclusion; + keysOk.in[1] <== inverse(auxIsEmpty); + keysOk.in[2] <== areKeyEquals.out; + keysOk.out === 0; + + component auxHash = Hash3(); + auxHash.a <== auxKey; + auxHash.b <== auxValue; + auxHash.c <== 1; + + component hash = Hash3(); + hash.a <== key; + hash.b <== value; + hash.c <== 1; + + component keyBits = Num2Bits_strict(); + keyBits.in <== key; + + component depths = DepthDeterminer(depth); + + for (var i = 0; i < depth; i++) { + depths.siblings[i] <== siblings[i]; + } + + component nodeType[depth]; + + // Start with the middle node (closest to the root) + for (var i = 0; i < depth; i++) { + nodeType[i] = NodeTypeDeterminer(); + + if (i == 0) { + nodeType[i].previousMiddle <== 1; + nodeType[i].previousEmpty <== 0; + nodeType[i].previousLeaf <== 0; + nodeType[i].previousAuxLeaf <== 0; + } else { + nodeType[i].previousMiddle <== nodeType[i - 1].middle; + nodeType[i].previousEmpty <== nodeType[i - 1].empty; + nodeType[i].previousLeaf <== nodeType[i - 1].leaf; + nodeType[i].previousAuxLeaf <== nodeType[i - 1].auxLeaf; + } + + nodeType[i].auxIsEmpty <== auxIsEmpty; + nodeType[i].isExclusion <== isExclusion; + nodeType[i].isDesiredDepth <== depths.desiredDepth[i]; + } + + component depthHash[depth]; + + // Hash up the elements in the reverse order + for (var i = depth - 1; i >= 0; i--) { + depthHash[i] = DepthHasher(); + + depthHash[i].isMiddle <== nodeType[i].middle; + depthHash[i].isLeaf <== nodeType[i].leaf; + depthHash[i].isAuxLeaf <== nodeType[i].auxLeaf; + + depthHash[i].sibling <== siblings[i]; + depthHash[i].auxLeaf <== auxHash.out; + depthHash[i].leaf <== hash.out; + + depthHash[i].currentKeyBit <== keyBits.out[i]; + + if (i == depth - 1) { + // The last depth has no child + depthHash[i].child <== 0; + } else { + // The child of the current depth is the root of the next depth + depthHash[i].child <== depthHash[i + 1].root; + } + } + + // The root of the merkle tree is the root of the first depth + depthHash[0].root === root; +} diff --git a/assets/erc-7812/contracts/SparseMerkleTree.sol b/assets/erc-7812/contracts/SparseMerkleTree.sol new file mode 100644 index 0000000000..59325c4d04 --- /dev/null +++ b/assets/erc-7812/contracts/SparseMerkleTree.sol @@ -0,0 +1,671 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {IEvidenceDB} from "../interfaces/IEvidenceDB.sol"; + +/** + * @notice Sparse Merkle Tree implementation. + */ +library SparseMerkleTree { + /** + * @dev A maximum depth hard cap for SMT + * Due to the limitations of the uint256 data type, depths greater than 256 are not possible. + */ + uint16 internal constant MAX_DEPTH_HARD_CAP = 256; + + uint64 internal constant ZERO_IDX = 0; + + bytes32 internal constant ZERO_HASH = bytes32(0); + + /** + * @notice The type of the node in the Merkle tree. + */ + enum NodeType { + EMPTY, + LEAF, + MIDDLE + } + + /** + * @notice Defines the structure of the Sparse Merkle Tree. + * + * @param nodes A mapping of the tree's nodes, where the key is the node's index, starting from 1 upon node addition. + * This approach differs from the original implementation, which utilized a hash as the key: + * H(k || v || 1) for leaf nodes and H(left || right) for middle nodes. + * + * @param merkleRootId The index of the root node. + * @param maxDepth The maximum depth of the Merkle tree. + * @param nodesCount The total number of nodes within the Merkle tree. + * @param customHasherSet Indicates whether custom hash functions have been configured (true) or not (false). + * @param hash2 A hash function accepting two arguments. + * @param hash3 A hash function accepting three arguments. + */ + struct SMT { + mapping(uint256 => Node) nodes; + uint64 merkleRootId; + uint64 nodesCount; + uint64 deletedNodesCount; + uint32 maxDepth; + bool customHasherSet; + function(bytes32, bytes32) view returns (bytes32) hash2; + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3; + } + + /** + * @notice Describes a node within the Merkle tree, including its type, children, hash, and key-value pair. + * + * @param nodeType The type of the node. + * @param childLeft The index of the left child node. + * @param childRight The index of the right child node. + * @param nodeHash The hash of the node, calculated as follows: + * - For leaf nodes, H(k || v || 1) where k is the key and v is the value; + * - For middle nodes, H(left || right) where left and right are the hashes of the child nodes. + * + * @param key The key associated with the node. + * @param value The value associated with the node. + */ + struct Node { + NodeType nodeType; + uint64 childLeft; + uint64 childRight; + bytes32 nodeHash; + bytes32 key; + bytes32 value; + } + + modifier onlyInitialized(SMT storage tree) { + if (!_isInitialized(tree)) revert TreeNotInitialized(); + _; + } + + error KeyAlreadyExists(bytes32 key); + error LeafDoesNotMatch(bytes32 currentKey, bytes32 key); + error MaxDepthExceedsHardCap(uint32 maxDepth); + error MaxDepthIsZero(); + error MaxDepthReached(); + error NewMaxDepthMustBeLarger(uint32 currentDepth, uint32 newDepth); + error NodeDoesNotExist(uint256 nodeId); + error TreeAlreadyInitialized(); + error TreeNotInitialized(); + error TreeIsNotEmpty(); + + /** + * @notice The function to initialize the Merkle tree. + * Under the hood it sets the maximum depth of the Merkle tree, therefore can be considered + * alias function for the `setMaxDepth`. + * + * Requirements: + * - The current tree depth must be 0. + * + * @param tree self. + * @param maxDepth_ The max depth of the Merkle tree. + */ + function initialize(SMT storage tree, uint32 maxDepth_) internal { + if (_isInitialized(tree)) revert TreeAlreadyInitialized(); + + _setMaxDepth(tree, maxDepth_); + } + + /** + * @notice The function to set the maximum depth of the Merkle tree. Complexity is O(1). + * + * Requirements: + * - The max depth must be greater than zero. + * - The max depth can only be increased. + * - The max depth is less than or equal to MAX_DEPTH_HARD_CAP (256). + * + * @param tree self. + * @param maxDepth_ The max depth of the Merkle tree. + */ + function setMaxDepth(SMT storage tree, uint32 maxDepth_) internal { + _setMaxDepth(tree, maxDepth_); + } + + /** + * @notice The function to set a custom hash functions, that will be used to build the Merkle Tree. + * + * Requirements: + * - The tree must be empty. + * + * @param tree self. + * @param hash2_ The hash function that accepts two argument. + * @param hash3_ The hash function that accepts three arguments. + */ + function setHashers( + SMT storage tree, + function(bytes32, bytes32) view returns (bytes32) hash2_, + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3_ + ) internal { + if (_nodesCount(tree) != 0) revert TreeIsNotEmpty(); + + tree.customHasherSet = true; + + tree.hash2 = hash2_; + tree.hash3 = hash3_; + } + + /** + * @notice The function to add a new element to the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @param value_ The value of the element. + */ + function add(SMT storage tree, bytes32 key_, bytes32 value_) internal onlyInitialized(tree) { + _add(tree, key_, value_); + } + + /** + * @notice The function to remove a (leaf) element from the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + */ + function remove(SMT storage tree, bytes32 key_) internal onlyInitialized(tree) { + tree.merkleRootId = uint64(_remove(tree, key_, tree.merkleRootId, 0)); + } + + /** + * @notice The function to update a (leaf) element in the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @param newValue_ The new value of the element. + */ + function update( + SMT storage tree, + bytes32 key_, + bytes32 newValue_ + ) internal onlyInitialized(tree) { + _update(tree, key_, newValue_); + } + + /** + * @notice The function to get the proof if a node with specific key exists or not exists in the SMT. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @return SMT proof struct. + */ + function getProof( + SMT storage tree, + bytes32 key_ + ) internal view returns (IEvidenceDB.Proof memory) { + uint256 maxDepth_ = _maxDepth(tree); + + IEvidenceDB.Proof memory proof_ = IEvidenceDB.Proof({ + root: _root(tree), + siblings: new bytes32[](maxDepth_), + existence: false, + key: key_, + value: ZERO_HASH, + auxExistence: false, + auxKey: ZERO_HASH, + auxValue: ZERO_HASH + }); + + Node memory node_; + uint256 nextNodeId_ = tree.merkleRootId; + + for (uint256 i = 0; i <= maxDepth_; i++) { + node_ = _node(tree, nextNodeId_); + + if (node_.nodeType == NodeType.EMPTY) { + break; + } else if (node_.nodeType == NodeType.LEAF) { + if (node_.key == proof_.key) { + proof_.existence = true; + proof_.value = node_.value; + + break; + } else { + proof_.auxExistence = true; + proof_.auxKey = node_.key; + proof_.auxValue = node_.value; + proof_.value = node_.value; + + break; + } + } else { + if ((uint256(proof_.key) >> i) & 1 == 1) { + nextNodeId_ = node_.childRight; + + proof_.siblings[i] = tree.nodes[node_.childLeft].nodeHash; + } else { + nextNodeId_ = node_.childLeft; + + proof_.siblings[i] = tree.nodes[node_.childRight].nodeHash; + } + } + } + + return proof_; + } + + /** + * @notice The function to get the root of the Merkle tree. + * Complexity is O(1). + * + * @param tree self. + * @return The root of the Merkle tree. + */ + function getRoot(SMT storage tree) internal view returns (bytes32) { + return _root(tree); + } + + /** + * @notice The function to get the node by its index. + * Complexity is O(1). + * + * @param tree self. + * @param nodeId_ The index of the node. + * @return The node. + */ + function getNode(SMT storage tree, uint256 nodeId_) internal view returns (Node memory) { + return _node(tree, nodeId_); + } + + /** + * @notice The function to get the node by its key. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @return The node. + */ + function getNodeByKey(SMT storage tree, bytes32 key_) internal view returns (Node memory) { + Node memory node_; + uint256 nextNodeId_ = tree.merkleRootId; + + for (uint256 i = 0; i <= tree.maxDepth; i++) { + node_ = tree.nodes[nextNodeId_]; + + if (node_.nodeType == NodeType.EMPTY) { + break; + } else if (node_.nodeType == NodeType.LEAF) { + if (node_.key == key_) { + break; + } + } else { + if ((uint256(key_) >> i) & 1 == 1) { + nextNodeId_ = node_.childRight; + } else { + nextNodeId_ = node_.childLeft; + } + } + } + + return + node_.key == key_ + ? node_ + : Node({ + nodeType: NodeType.EMPTY, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + /** + * @notice The function to get the max depth of the Merkle tree. + * + * @param tree self. + * @return The max depth of the Merkle tree. + */ + function getMaxDepth(SMT storage tree) internal view returns (uint64) { + return uint64(_maxDepth(tree)); + } + + /** + * @notice The function to get the number of nodes in the Merkle tree. + * + * @param tree self. + * @return The number of nodes in the Merkle tree. + */ + function getNodesCount(SMT storage tree) internal view returns (uint64) { + return uint64(_nodesCount(tree)); + } + + /** + * @notice The function to check if custom hash functions are set. + * + * @param tree self. + * @return True if custom hash functions are set, otherwise false. + */ + function isCustomHasherSet(SMT storage tree) internal view returns (bool) { + return tree.customHasherSet; + } + + function _setMaxDepth(SMT storage tree, uint32 maxDepth_) private { + if (maxDepth_ == 0) revert MaxDepthIsZero(); + + uint32 currentDepth_ = tree.maxDepth; + + if (maxDepth_ <= currentDepth_) revert NewMaxDepthMustBeLarger(currentDepth_, maxDepth_); + if (maxDepth_ > MAX_DEPTH_HARD_CAP) revert MaxDepthExceedsHardCap(maxDepth_); + + tree.maxDepth = maxDepth_; + } + + function _add(SMT storage tree, bytes32 key_, bytes32 value_) private { + Node memory node_ = Node({ + nodeType: NodeType.LEAF, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: key_, + value: value_ + }); + + tree.merkleRootId = uint64(_add(tree, node_, tree.merkleRootId, 0)); + } + + function _update(SMT storage tree, bytes32 key_, bytes32 newValue_) private { + Node memory node_ = Node({ + nodeType: NodeType.LEAF, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: key_, + value: newValue_ + }); + + _update(tree, node_, tree.merkleRootId, 0); + } + + /** + * @dev The check for whether the current depth exceeds the maximum depth is omitted for two reasons: + * 1. The current depth may only surpass the maximum depth during the addition of a new leaf. + * 2. As we navigate through middle nodes, the current depth is assured to remain below the maximum + * depth since the traversal must ultimately conclude at a leaf node. + */ + function _add( + SMT storage tree, + Node memory newLeaf_, + uint256 nodeId_, + uint16 currentDepth_ + ) private returns (uint256) { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + return _setNode(tree, newLeaf_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key == newLeaf_.key) revert KeyAlreadyExists(newLeaf_.key); + + return _pushLeaf(tree, newLeaf_, currentNode_, nodeId_, currentDepth_); + } else { + uint256 nextNodeId_; + + if ((uint256(newLeaf_.key) >> currentDepth_) & 1 == 1) { + nextNodeId_ = _add(tree, newLeaf_, currentNode_.childRight, currentDepth_ + 1); + + tree.nodes[nodeId_].childRight = uint64(nextNodeId_); + } else { + nextNodeId_ = _add(tree, newLeaf_, currentNode_.childLeft, currentDepth_ + 1); + + tree.nodes[nodeId_].childLeft = uint64(nextNodeId_); + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, tree.nodes[nodeId_]); + + return nodeId_; + } + } + + function _remove( + SMT storage tree, + bytes32 key_, + uint256 nodeId_, + uint16 currentDepth_ + ) private returns (uint256) { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + revert NodeDoesNotExist(nodeId_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key != key_) revert LeafDoesNotMatch(currentNode_.key, key_); + + _deleteNode(tree, nodeId_); + + return ZERO_IDX; + } else { + uint256 nextNodeId_; + + if ((uint256(key_) >> currentDepth_) & 1 == 1) { + nextNodeId_ = _remove(tree, key_, currentNode_.childRight, currentDepth_ + 1); + } else { + nextNodeId_ = _remove(tree, key_, currentNode_.childLeft, currentDepth_ + 1); + } + + NodeType rightType_ = tree.nodes[currentNode_.childRight].nodeType; + NodeType leftType_ = tree.nodes[currentNode_.childLeft].nodeType; + + if (rightType_ == NodeType.EMPTY && leftType_ == NodeType.EMPTY) { + _deleteNode(tree, nodeId_); + + return nextNodeId_; + } + + NodeType nextType_ = tree.nodes[nextNodeId_].nodeType; + + if ( + (rightType_ == NodeType.EMPTY || leftType_ == NodeType.EMPTY) && + nextType_ != NodeType.MIDDLE + ) { + if ( + nextType_ == NodeType.EMPTY && + (leftType_ == NodeType.LEAF || rightType_ == NodeType.LEAF) + ) { + _deleteNode(tree, nodeId_); + + if (rightType_ == NodeType.LEAF) { + return currentNode_.childRight; + } + + return currentNode_.childLeft; + } + + if (rightType_ == NodeType.EMPTY) { + tree.nodes[nodeId_].childRight = uint64(nextNodeId_); + } else { + tree.nodes[nodeId_].childLeft = uint64(nextNodeId_); + } + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, tree.nodes[nodeId_]); + + return nodeId_; + } + } + + function _update( + SMT storage tree, + Node memory newLeaf_, + uint256 nodeId_, + uint16 currentDepth_ + ) private { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + revert NodeDoesNotExist(nodeId_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key != newLeaf_.key) + revert LeafDoesNotMatch(currentNode_.key, newLeaf_.key); + + tree.nodes[nodeId_] = newLeaf_; + currentNode_ = newLeaf_; + } else { + if ((uint256(newLeaf_.key) >> currentDepth_) & 1 == 1) { + _update(tree, newLeaf_, currentNode_.childRight, currentDepth_ + 1); + } else { + _update(tree, newLeaf_, currentNode_.childLeft, currentDepth_ + 1); + } + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, currentNode_); + } + + function _pushLeaf( + SMT storage tree, + Node memory newLeaf_, + Node memory oldLeaf_, + uint256 oldLeafId_, + uint16 currentDepth_ + ) private returns (uint256) { + if (currentDepth_ >= tree.maxDepth) revert MaxDepthReached(); + + Node memory newNodeMiddle_; + bool newLeafBitAtDepth_ = (uint256(newLeaf_.key) >> currentDepth_) & 1 == 1; + bool oldLeafBitAtDepth_ = (uint256(oldLeaf_.key) >> currentDepth_) & 1 == 1; + + // Check if we need to go deeper if diverge at the depth's bit + if (newLeafBitAtDepth_ == oldLeafBitAtDepth_) { + uint256 nextNodeId_ = _pushLeaf( + tree, + newLeaf_, + oldLeaf_, + oldLeafId_, + currentDepth_ + 1 + ); + + if (newLeafBitAtDepth_) { + // go right + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: ZERO_IDX, + childRight: uint64(nextNodeId_), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } else { + // go left + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(nextNodeId_), + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + return _setNode(tree, newNodeMiddle_); + } + + uint256 newLeafId = _setNode(tree, newLeaf_); + + if (newLeafBitAtDepth_) { + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(oldLeafId_), + childRight: uint64(newLeafId), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } else { + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(newLeafId), + childRight: uint64(oldLeafId_), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + return _setNode(tree, newNodeMiddle_); + } + + /** + * @dev The function used to add new nodes. + */ + function _setNode(SMT storage tree, Node memory node_) private returns (uint256) { + node_.nodeHash = _getNodeHash(tree, node_); + + uint256 newCount_ = ++tree.nodesCount; + tree.nodes[newCount_] = node_; + + return newCount_; + } + + /** + * @dev The function used to delete removed nodes. + */ + function _deleteNode(SMT storage tree, uint256 nodeId_) private { + delete tree.nodes[nodeId_]; + ++tree.deletedNodesCount; + } + + /** + * @dev The check for an empty node is omitted, as this function is called only with + * non-empty nodes and is not intended for external use. + */ + function _getNodeHash(SMT storage tree, Node memory node_) private view returns (bytes32) { + function(bytes32, bytes32) view returns (bytes32) hash2_ = tree.customHasherSet + ? tree.hash2 + : _hash2; + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3_ = tree.customHasherSet + ? tree.hash3 + : _hash3; + + if (node_.nodeType == NodeType.LEAF) { + return hash3_(node_.key, node_.value, bytes32(uint256(1))); + } + + return hash2_(tree.nodes[node_.childLeft].nodeHash, tree.nodes[node_.childRight].nodeHash); + } + + function _hash2(bytes32 a, bytes32 b) private pure returns (bytes32 result) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + mstore(0, a) + mstore(32, b) + + result := keccak256(0, 64) + } + } + + /** + * @dev The decision not to update the free memory pointer is due to the temporary nature of the hash arguments. + */ + function _hash3(bytes32 a, bytes32 b, bytes32 c) private pure returns (bytes32 result) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + let free_ptr := mload(64) + + mstore(free_ptr, a) + mstore(add(free_ptr, 32), b) + mstore(add(free_ptr, 64), c) + + result := keccak256(free_ptr, 96) + } + } + + function _root(SMT storage tree) private view returns (bytes32) { + return tree.nodes[tree.merkleRootId].nodeHash; + } + + function _node(SMT storage tree, uint256 nodeId_) private view returns (Node memory) { + return tree.nodes[nodeId_]; + } + + function _maxDepth(SMT storage tree) private view returns (uint256) { + return tree.maxDepth; + } + + function _nodesCount(SMT storage tree) private view returns (uint256) { + return tree.nodesCount - tree.deletedNodesCount; + } + + function _isInitialized(SMT storage tree) private view returns (bool) { + return tree.maxDepth > 0; + } +} diff --git a/assets/erc-7812/images/diagram.png b/assets/erc-7812/images/diagram.png new file mode 100644 index 0000000000..4f1eb9948e Binary files /dev/null and b/assets/erc-7812/images/diagram.png differ diff --git a/assets/erc-7837/DiffusiveToken.sol b/assets/erc-7837/DiffusiveToken.sol new file mode 100644 index 0000000000..7661ea8eee --- /dev/null +++ b/assets/erc-7837/DiffusiveToken.sol @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.0; + +/** + * @title DiffusiveToken + * @author + * @notice An ERC-20-like token that mints new tokens to the recipient on each transfer, + * does not reduce the sender's balance, requires a native fee per token transferred, + * and caps the total supply at a maximum value. Holders can burn tokens to reduce supply. + */ + +contract DiffusiveToken { + // ----------------------------------------- + // State Variables + // ----------------------------------------- + + string public name; + string public symbol; + uint8 public decimals; + + uint256 public totalSupply; + uint256 public maxSupply; + uint256 public transferFee; // Fee per token transferred in wei + + address public owner; + + mapping(address => uint256) private balances; + mapping(address => mapping(address => uint256)) private allowances; + + // ----------------------------------------- + // Events + // ----------------------------------------- + + event Transfer(address indexed from, address indexed to, uint256 amount); + event Burn(address indexed burner, uint256 amount); + event FeeUpdated(uint256 newFee); + event MaxSupplyUpdated(uint256 newMaxSupply); + event Approval(address indexed owner, address indexed spender, uint256 value); + + // ----------------------------------------- + // Modifiers + // ----------------------------------------- + + modifier onlyOwner() { + require(msg.sender == owner, "DiffusiveToken: caller is not the owner"); + _; + } + + // ----------------------------------------- + // Constructor + // ----------------------------------------- + + /** + * @dev Constructor sets the initial parameters for the Diffusive Token. + * @param _name Token name + * @param _symbol Token symbol + * @param _decimals Decimal places + * @param _maxSupply The max supply of tokens that can ever exist + * @param _transferFee Initial fee per token transferred in wei + */ + constructor( + string memory _name, + string memory _symbol, + uint8 _decimals, + uint256 _maxSupply, + uint256 _transferFee + ) { + name = _name; + symbol = _symbol; + decimals = _decimals; + maxSupply = _maxSupply; + transferFee = _transferFee; + owner = msg.sender; + totalSupply = 0; // Initially, no tokens are minted + } + + // ----------------------------------------- + // External and Public Functions + // ----------------------------------------- + + /** + * @notice Returns the token balance of the given address. + * @param account The address to query + */ + function balanceOf(address account) external view returns (uint256) { + return balances[account]; + } + + /** + * @notice Transfers `amount` tokens to address `to`, minting new tokens in the process. + * @dev Requires payment of native currency: transferFee * amount. + * @param to Recipient address + * @param amount Number of tokens to transfer + * @return True if successful + */ + function transfer(address to, uint256 amount) external payable returns (bool) { + require(to != address(0), "DiffusiveToken: transfer to zero address"); + require(amount > 0, "DiffusiveToken: amount must be greater than zero"); + + uint256 requiredFee = transferFee * amount; + require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee"); + + // Check max supply limit + require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply"); + + // Mint new tokens to `to` + balances[to] += amount; + totalSupply += amount; + + emit Transfer(msg.sender, to, amount); + return true; + } + + /** + * @notice Burns `amount` tokens from the caller's balance, decreasing total supply. + * @param amount The number of tokens to burn + */ + function burn(uint256 amount) external { + require(amount > 0, "DiffusiveToken: burn amount must be greater than zero"); + require(balances[msg.sender] >= amount, "DiffusiveToken: insufficient balance"); + + balances[msg.sender] -= amount; + totalSupply -= amount; + + emit Burn(msg.sender, amount); + } + + /** + * @notice Approves `spender` to transfer up to `amount` tokens on behalf of `msg.sender`. + * @param spender The address authorized to spend + * @param amount The max amount they can spend + */ + function approve(address spender, uint256 amount) external returns (bool) { + require(spender != address(0), "DiffusiveToken: approve to zero address"); + allowances[msg.sender][spender] = amount; + emit Approval(msg.sender, spender, amount); + return true; + } + + /** + * @notice Returns the current allowance of `spender` for `owner`. + * @param _owner The owner of the tokens + * @param _spender The address allowed to spend the tokens + */ + function allowance(address _owner, address _spender) external view returns (uint256) { + return allowances[_owner][_spender]; + } + + /** + * @notice Transfers `amount` tokens from `from` to `to` using the allowance mechanism. + * @dev The `from` account does not lose tokens; this still mints to `to`. + * @param from The address from which the allowance has been given + * @param to The recipient address + * @param amount The number of tokens to transfer (mint) + */ + function transferFrom(address from, address to, uint256 amount) external payable returns (bool) { + require(to != address(0), "DiffusiveToken: transfer to zero address"); + require(amount > 0, "DiffusiveToken: amount must be greater than zero"); + + uint256 allowed = allowances[from][msg.sender]; + require(allowed >= amount, "DiffusiveToken: allowance exceeded"); + + // Deduct from allowance + allowances[from][msg.sender] = allowed - amount; + + uint256 requiredFee = transferFee * amount; + require(msg.value >= requiredFee, "DiffusiveToken: insufficient fee"); + + // Check max supply + require(totalSupply + amount <= maxSupply, "DiffusiveToken: would exceed max supply"); + + // Mint tokens to `to` + balances[to] += amount; + totalSupply += amount; + + emit Transfer(from, to, amount); + return true; + } + + // ----------------------------------------- + // Owner Functions + // ----------------------------------------- + + /** + * @notice Updates the maximum supply of tokens. Must be >= current totalSupply. + * @param newMaxSupply The new maximum supply + */ + function setMaxSupply(uint256 newMaxSupply) external onlyOwner { + require(newMaxSupply >= totalSupply, "DiffusiveToken: new max < current supply"); + maxSupply = newMaxSupply; + emit MaxSupplyUpdated(newMaxSupply); + } + + /** + * @notice Updates the per-token transfer fee. + * @param newFee The new fee in wei per token transferred + */ + function setTransferFee(uint256 newFee) external onlyOwner { + transferFee = newFee; + emit FeeUpdated(newFee); + } + + /** + * @notice Allows the owner to withdraw accumulated native currency fees. + * @param recipient The address that will receive the withdrawn fees + */ + function withdrawFees(address payable recipient) external onlyOwner { + require(recipient != address(0), "DiffusiveToken: withdraw to zero address"); + uint256 balance = address(this).balance; + (bool success, ) = recipient.call{value: balance}(""); + require(success, "DiffusiveToken: withdrawal failed"); + } + + // ----------------------------------------- + // Fallback and Receive + // ----------------------------------------- + + // Allows the contract to receive Ether. + receive() external payable {} +} \ No newline at end of file