diff --git a/ERCS/erc-7579.md b/ERCS/erc-7579.md index 8730fe2276..6b05388a79 100644 --- a/ERCS/erc-7579.md +++ b/ERCS/erc-7579.md @@ -68,6 +68,8 @@ interface IExecution { * @param mode The encoded execution mode of the transaction. * @param executionCalldata The encoded execution call data. * + * @return returnData An array with the returned data of each executed subcall + * * MUST ensure adequate authorization control: i.e. onlyExecutorModule * If a mode is requested that is not supported by the Account, it MUST revert */ @@ -91,9 +93,10 @@ The account MAY also implement the following function in accordance with ERC-433 function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; ``` -If an account chooses to implement `executeUserOp`, this method SHOULD ensure the account executes `userOp.calldata` except 4 most significant bytes, which are reserved for `executeUserOp.selector` as per ERC-4337. Thus the `userOp.callData[4:]` should represent the calldata for a valid call to the account. It is RECOMMENDED that the account executes a `delegatecall` in order to preserve the original `msg.sender` to the account. +If an account chooses to implement `executeUserOp`, this method SHOULD ensure the account executes `userOp.calldata` except 4 most significant bytes, which are reserved for `executeUserOp.selector` as per ERC-4337. Thus the `userOp.callData[4:]` should represent the calldata for a valid call to the account. It is RECOMMENDED that the account executes a `delegatecall` in order to preserve the original `msg.sender` to the account. Example: + ``` (bool success, bytes memory innerCallRet) = address(this).delegatecall(userOp.callData[4:]); ``` @@ -228,7 +231,7 @@ If the smart account has a fallback handler installed, it: - MUST use `call` or `staticcall` to invoke the fallback handler - MUST utilize [ERC-2771](./eip-2771.md) to add the original `msg.sender` to the `calldata` sent to the fallback handler - MUST route to fallback handlers based on the function selector of the calldata -- MAY implement authorization control, which SHOULD be done via hooks +- MAY implement authorization control, which SHOULD be done via hooks If the account adds features via fallback, these should be considered the same as if the account was implementing those features natively. ERC-165 support (see below) is one example of such an approach. Note, that it is only RECOMMENDED to implement view functions via fallback where this can lead to greater extensibility. It is NOT RECOMMENDED to implement core account logic via a fallback. @@ -280,6 +283,7 @@ interface IModule { Note: A single module that is of multiple types MAY decide to pass `moduleTypeId` inside `data` to `onInstall` and/or `onUninstall` methods, so those methods are able to properly handle installation/uninstallation for various types. Example: + ```solidity // Module.sol function onInstall(bytes calldata data) external { @@ -367,7 +371,7 @@ Our approach has been twofold: ### Extensions -While we want to be minimal, we also want to allow for innovation and opinionated features. Some of these features might also need to be standardized (for similar reasons as the core interfaces) even if not all smart accounts will implement them. To ensure that this is possible, we suggest for future standardization efforts to be done as extensions to this standard. This means that the core interfaces will not change, but that new interfaces can be added as extensions. These should be proposed as separate ERCs, for example with the title `[FEATURE] Extension for ERC-7579`. +While we want to be minimal, we also want to allow for innovation and opinionated features. Some of these features might also need to be standardized (for similar reasons as the core interfaces) even if not all smart accounts will implement them. To ensure that this is possible, we suggest for future standardization efforts to be done as extensions to this standard. This means that the core interfaces will not change, but that new interfaces can be added as extensions. These should be proposed as separate ERCs, for example with the title "[FEATURE] Extension for [ERC-7579](./eip-7579.md)". ### Specification diff --git a/ERCS/erc-7694.md b/ERCS/erc-7694.md new file mode 100644 index 0000000000..fc1be4506c --- /dev/null +++ b/ERCS/erc-7694.md @@ -0,0 +1,198 @@ +--- +eip: 7694 +title: Solana Storage Router +description: Cross-chain storage router protocol incorporating storage router for Solana +author: Avneet Singh (@sshmatrix), 0xc0de4c0ffee (@0xc0de4c0ffee) +discussions-to: https://ethereum-magicians.org/t/erc-7694-solana-storage-router/19706 +status: Draft +type: Standards Track +category: ERC +created: 2024-04-18 +requires: 3668, 7700 +--- + +## Abstract + +The following standard is an extension to the cross-chain storage router protocol introducing the storage router for Solana blockchain. With this specification, any Ethereum L1 contract can defer a call to Solana blockchain as part of its core functionality, provided that the client is equipped to handle Solana transactions. It was previously possible to defer write and storage operations to other Ethereum L1 contracts, L2 contracts and off-chain databases, and this document extends that functionality to include alternative L1 chains. The data stored on Solana must be translated to [EIP-3668](./eip-3668)-compliant format by an appropriate HTTP gateway where it can be retrieved by generic Ethereum contracts. This standard allows Ethereum to utilise a broader range of cross-chain blockspaces. + +## Motivation + +Cross-Chain Storage Router Protocol (CCIP-Store) introduced in [EIP-7700](./eip-7700), describes three external routers for routing storage to L1 contracts, L2s and databases. This document extends that specification by introducing a fourth storage router targeting Solana as the storage provider. + +L2s and databases both have centralising catalysts in their stack. For L2s, this centralising agent is the shared security with Ethereum mainnet. In case of databases, the centralising agent is trivial; it is the physical server hosting the database. In light of this, a storage provider that relies on its own independent consensus mechanism is preferred. This specification instructs how the clients should treat storage calls made to the Solana router. + +Solana is a low cost L1 solution that is supported alongside Ethereum by multiple wallet providers. There are several chain-agnostic protocols on Ethereum which could benefit from direct access to Solana blockspace; ENS is one such example where it can serve users of Solana via its chain-agnostic properties while also using Solana's own native storage. This development will encourage more cross-chain functionalities between Ethereum and Solana at core. + +![Fig.1 CCIP-Store and CCIP-Read Workflows](../assets/eip-7694/images/Schema.svg) + +## Specification + +A Solana storage router `StorageRoutedToSolana()` requires the hex-encoded `programId` and the manager `account` on the Solana blockchain. `programId` is equivalent to a contract address on Solana while `account` is the manager wallet on Solana handling storage on behalf of `msg.sender`. + +```solidity +// Revert handling Solana storage router +error StorageRoutedToSolana( + bytes32 programId, + bytes32 account +); + +// Generic function in a contract +function setValue( + bytes32 node, + bytes32 key, + bytes32 value +) external { + // Get metadata from on-chain sources + ( + bytes32 programId, // Program (= contract) address on Solana; hex-encoded + bytes32 account // Manager account on Solana; hex-encoded + ) = getMetadata(node); // Arbitrary code + // programId = 0x37868885bbaf236c5d2e7a38952f709e796a1c99d6c9d142a1a41755d7660de3 + // account = 0xe853e0dcc1e57656bd760325679ea960d958a0a704274a5a12330208ba0f428f + // Route storage call to Solana router + revert StorageRoutedToSolana( + programId, + account + ); +}; +``` + +Since Solana natively uses `base58` encoding in its virtual machine setup, `programId` values that are hex-encoded on EVM must be `base58`-decoded for usage on SVM. Clients implementing the Solana router must call the Solana `programId` using a Solana wallet that is connected to `account` using the `base58`-decoded (and casted to appropriate data type) calldata that it originally received. + +```js +/* Pseudo-code to write to Solana program (= contract) */ +// Decode all 'bytes32' types in EVM to 'PubKey' type in SVM +const [programId, account, node, key, value] = E2SVM( + [programId, account, node, key, value], + ["bytes32", "bytes32", "bytes32", "bytes32", "bytes32"] +); +// Instantiate program interface on Solana +const program = new program(programId, rpcProvider); +// Connect to Solana wallet +const wallet = useWallet(); +// Call the Solana program using connected wallet with initial calldata +// [!] Only approved manager in the Solana program should call +if (wallet.publicKey === account) { + await program(wallet).setValue(node, key, value); +} +``` + +In the above example, EVM-specific `bytes32`-type variables `programId`, `account`, `node`, `key` and `value` must all be converted to SVM-specific `PubKey` data type. The equivalent `setValue()` function in the Solana program is of the form + +```rust +// Example function in Solana program +pub fn setValue( + ctx: Context, + node: PubKey, + key: PubKey, + value: PubKey +) -> ProgramResult { + // Code to verify PROGRAM_ID and rent exemption status + ... + // Code for de-serialising, updating and re-serialising the data + ... + // Store serialised data in account + // [!] Stored data must be mapped by node & account + ... +} +``` + +Since EVM and SVM have differing architectures, it is important to define precise data type castings from EVM to SVM. Some pre-existing custom but popular data types in SVM already equate to common EVM data types such as `PubKey` and `bytes32` respectively. This specification requires the following implementation of bijective EVM to SVM type casting: + +| EVM | SVM | +| :-------: | :---------------: | +| `uint8` | `u8` | +| `uint16` | `u16` | +| `uint32` | `u32` | +| `uint64` | `u64` | +| `uint128` | `u128` | +| `uint256` | `u256`† | +| `bytes1` | `bytes: [u8; 1]` | +| `bytes2` | `bytes: [u8; 2]` | +| `bytes4` | `bytes: [u8; 4]` | +| `bytes8` | `bytes: [u8; 8]` | +| `bytes16` | `bytes: [u8; 16]` | +| `bytes32` | `PubKey` | +| `bytes` | `bytes: Vec` | +| `string` | `String` | +| `address` | `bytes: [u8; 20]` | + +> † `u256` is not available natively in SVM but is routinely implemented via `u256` crate in Rust + +Using this strategy, most - if not all - current use-cases of `StorageRoutedToSolana()` are accounted for. + +Finally, in order to read the cross-chain data stored on Solana in an arbitrary Ethereum contract, it must be translated back into EVM tongue by an [EIP-3668](./eip-3668)-compliant HTTP gateway. The arguments for a generic call to the gateway URL must be specified in the `/`-delimited nested format as described in [EIP-7700](./eip-7700). The core of such a gateway must follow + +```js +/* Pseudo-code of an ERC-3668-compliant HTTP gateway tunneling Solana content to Ethereum */ +// CCIP-Read call by contract to a known gateway URL; gatewayUrl = 'https://read.solana.namesys.xyz////' +const [programId, node, key] = parseQuery(path); // Parse query parameters from path; path = '////' +// Decode 'bytes32' types in EVM to 'PubKey' type in SVM +const [programId, node, key] = E2SVM( + [programId, node, key], + ["bytes32", "bytes32", "bytes32"] +); +// Instantiate program interface on Solana +const program = new program(programId, rpcProvider); +// Call the Solana program to read in cross-chain data +const value = await program.getValue(node, key); +if (value !== "NOT_FOUND") { + // Decode 'PubKey' type in SVM to 'bytes32' type in EVM + const value = S2EVM(value, "PubKey"); +} else { + // Null value + const value = "0x0"; +} +// Compile CCIP-Read-compatible payload +const data = abi.encode(["bytes"], [value]); +// Create HTTP gateway emitting value in format 'data: ...' +emitERC3668(data); +``` + +In the above example, the generic `getValue()` function in the Solana program is of the form + +```rust +// Example getValue() function in Solana program +pub fn getValue<'a>( + ctx: Context, + node: Pubkey, + key: Pubkey, + account: &AccountInfo<'a>, // Lifetime-bound parameter +) -> Result { + // Validate that the account belongs to the correct program ID + ... + // Retrieve the data from the account + let data = &account.data.borrow(); + // De-serialise the data from the account + ... + // Look up the value by node and key + match deserialised.get(&node, &key) { + Some(value) => { + msg!("VALUE: {:?}", value); + Ok(value) + }, + None => { + msg!("NOT_FOUND"); + Err(ProgramError::InvalidArgument) + } + } +} +``` + +## Rationale + +`StorageRoutedToSolana()` works in a similar fashion to `StorageRoutedToL2()` in CCIP-Store in the sense that the client needs to be pointed to a certain contract on another chain by the revert event. Other than that, the only technical difference is casting between EVM and SVM data types. + +![Fig.2 Solana Call Lifecycle](../assets/eip-7694/images/Solana.svg) + +## Backwards Compatibility + +None. + +## Security Considerations + +None. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/ERCS/erc-7722.md b/ERCS/erc-7722.md index 29756670dd..d0ab06ca3f 100644 --- a/ERCS/erc-7722.md +++ b/ERCS/erc-7722.md @@ -2,7 +2,7 @@ eip: 7722 title: Opaque Token description: A token specification designed to enhance privacy by concealing balance information. -author: Ivica Aračić (@ivica7), SWIAT +author: Ivica Aračić (@ivica7), Ante Bešlić (@ethSplit), Mirko Katanić (@mkatanic), SWIAT discussions-to: https://ethereum-magicians.org/t/erc-7722-opaque-token/20249 status: Draft type: Standards Track diff --git a/ERCS/erc-7743.md b/ERCS/erc-7743.md new file mode 100755 index 0000000000..992b5349bc --- /dev/null +++ b/ERCS/erc-7743.md @@ -0,0 +1,394 @@ +--- +eip: 7743 +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 +type: Standards Track +category: ERC +created: 2024-07-13 +--- + +## Abstract + +This ERC proposes a new standard for non-fungible tokens (NFTs) that supports multiple owners. The MO-NFT standard allows a single NFT to have multiple owners, reflecting the shared and distributable nature of digital assets. This model incorporates mechanisms for provider-defined transfer fees and ownership burning, enabling flexible and collaborative ownership structures. It maintains compatibility with the existing [ERC-721](./eip-721.md) standard to ensure interoperability with current tools and platforms. + +## Motivation + +Traditional NFTs enforce a single-ownership model, which does not align with the inherent duplicability and collaborative potential of digital assets. MO-NFTs allow for shared ownership, promoting wider distribution and collaboration while maintaining secure access control. The inclusion of provider fees and ownership burning enhances the utility and flexibility of NFTs in representing digital assets and services. + +## 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**: + + - The function `mintToken()` allows the creation of a new MO-NFT. The caller becomes both the initial owner and the provider of the token. + + ```solidity + function mintToken() public onlyOwner returns (uint256); + ``` + + - A new `tokenId` is generated, and the caller is added to the owners set and recorded as the provider. The `balanceOf` the caller is incremented. + +2. **Ownership List**: + + - The MO-NFT maintains a list of owners for each token. Owners are stored in an enumerable set to prevent duplicates and allow efficient lookup. + +3. **Provider Role**: + + - The provider is the initial owner who can set and update the `transferValue` fee. Only the provider can modify certain token parameters. + +4. **Transfer Mechanism**: + + - Owners can transfer the token to new owners using `transferFrom`. The transfer adds the new owner to the list without removing existing owners and transfers the `transferValue` fee to the provider. + + ```solidity + function transferFrom(address from, address to, uint256 tokenId) public; + ``` + +### Transfer of Ownership + +1. **Additive Ownership**: + + - 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**: + + - 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**: + + - Owners can remove themselves from the ownership list using the `burn` function. + + ```solidity + 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** + +- `function mintToken() public onlyOwner returns (uint256);` + +- `function provide(string memory assetName, uint256 size, bytes32 fileHash, address provider, uint256 transferValue) external returns (uint256);` + +**Transfer Functions** + +- `function transferFrom(address from, address to, uint256 tokenId) public;` + +- `function safeTransferFrom(address from, address to, uint256 tokenId) public;` *(Disabled or overridden)* + +- `function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public;` *(Disabled or overridden)* + +**Ownership Management Functions** + +- `function isOwner(uint256 tokenId, address account) public view returns (bool);` + +- `function getOwnersCount(uint256 tokenId) public view returns (uint256);` + +- `function balanceOf(address owner) external view returns (uint256 balance);` + +- `function ownerOf(uint256 tokenId) external view returns (address owner);` + +**Provider Functions** + +- `function setTransferValue(uint256 tokenId, uint256 newTransferValue) external;` + +**Burn Function** + +- `function burn(uint256 tokenId) external;` + +### Events + +- `event TokenMinted(uint256 indexed tokenId, address indexed owner);` + +- `event TokenTransferred(uint256 indexed tokenId, address indexed from, address indexed to);` + +- `event TokenBurned(uint256 indexed tokenId, address indexed owner);` + +- `event TransferValueUpdated(uint256 indexed tokenId, uint256 oldTransferValue, uint256 newTransferValue);` + +### [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. + +- **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. + +- **Safe Transfer Functions**: The `safeTransferFrom` functions are restricted because traditional ERC-721 transfer safety checks are not applicable when ownership is additive rather than exclusive. + +- **Supports Interface**: The `supportsInterface` function ensures that the MO-NFT declares compatibility with the ERC-721 standard, allowing it to be integrated with existing tools and platforms. + + ```solidity + function supportsInterface(bytes4 interfaceId) public view returns (bool) { + return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC165).interfaceId; + } + ``` + +## Rationale + +1. **Multi-Ownership Model**: + + - Digital assets are inherently duplicable and can be shared without loss of quality. The multi-owner model allows broader distribution and collaboration while maintaining a unique token identity. + +2. **Additive Ownership**: + + - By adding new owners without removing existing ones, we support shared ownership models common in collaborative environments and digital content distribution. + +3. **Provider Fee Mechanism**: + + - Incorporating a provider fee incentivizes creators and providers by rewarding them whenever the asset is transferred. This aligns with models where creators receive royalties or fees for their work. + +4. **Ownership Burning**: + + - Allowing owners to remove themselves from the ownership list provides flexibility, enabling owners to relinquish rights or convert their digital ownership into real-world assets. + +5. **ERC-721 Compatibility**: + + - Maintaining compatibility with ERC-721 allows MO-NFTs to leverage existing infrastructure, tools, and platforms, facilitating adoption and interoperability. + +## Backwards Compatibility + +While the MO-NFT standard aims to maintain compatibility with ERC-721, certain deviations are necessary due to the multi-owner model: + +- **Approval Functions**: Disabled or overridden to prevent their use, as they do not align with the MO-NFT's ownership structure. + +- **Safe Transfer Functions**: Restricted because the additive ownership model does not fit the exclusive ownership assumptions in ERC-721. + +- **`ownerOf` Function**: Returns the first owner in the owners list for compatibility, but the concept of a single owner does not fully apply. + +Developers should be aware of these differences when integrating MO-NFTs into systems designed for standard ERC-721 tokens. + +## Test Cases + +1. **Minting an MO-NFT and Verifying Initial Ownership**: + + - **Input**: + + - Call `mintToken()` as the provider. + + - **Expected Output**: + + - A new `tokenId` is generated. + + - The caller is added as the first owner. + + - The `balanceOf` the caller increases by 1. + + - The provider is recorded for the token. + + - `TokenMinted` event is emitted. + +2. **Transferring an MO-NFT and Verifying Provider Fee Transfer**: + + - **Input**: + + - Call `transferFrom(from, to, tokenId)` where `from` is an existing owner and `to` is a new address. + + - **Expected Output**: + + - The `to` address is added to the owners list. + + - The `transferValue` fee is transferred to the provider. + + - The `balanceOf` of the `to` address increases by 1. + + - `TokenTransferred` event is emitted. + +3. **Burning Ownership**: + + - **Input**: + + - An owner calls `burn(tokenId)`. + + - **Expected Output**: + + - The owner is removed from the owners list. + + - The `balanceOf` of the owner decreases by 1. + + - If the owners list becomes empty, the token is effectively non-existent. + + - `TokenBurned` event is emitted. + +4. **Setting Transfer Value**: + + - **Input**: + + - The provider calls `setTransferValue(tokenId, newTransferValue)`. + + - **Expected Output**: + + - The `transferValue` is updated in the contract. + + - `TransferValueUpdated` event is emitted. + +5. **Failing Transfer to Existing Owner**: + + - **Input**: + + - Attempt to `transferFrom` to an address that is already an owner. + + - **Expected Output**: + + - The transaction reverts with the error `"MO-NFT: Recipient is already an owner"`. + + - No changes to ownership or balances occur. + + +## Reference Implementation + +The full reference implementation code for the MO-NFT standard is included in the EIPs repository under assets folder. This ensures the code is preserved alongside the EIP and remains accessible. + +- **Contracts**: + + - [`MONFT.sol`](../assets/eip-7743/MONFT.sol): The base implementation of the MO-NFT standard. + + - [`DigitalAsset.sol`](../assets/eip-7743/DigitalAsset.sol): An extended implementation for digital assets with provider fees. + +- **Interfaces**: + + - [`IDigitalAsset.sol`](../assets/eip-7743/IDigitalAsset.sol): Interface defining the functions for digital asset management. + +### Key Functions in Reference Implementation + +**Minting Tokens** + +```solidity +function mintToken() public onlyOwner returns (uint256) { + _nextTokenId++; + + // Add the sender to the set of owners for the new token + _owners[_nextTokenId].add(msg.sender); + + // Increment the balance of the owner + _balances[msg.sender] += 1; + + // Set the provider to the caller + _providers[_nextTokenId] = msg.sender; + + emit TokenMinted(_nextTokenId, msg.sender); + return _nextTokenId; +} +``` + +**Transferring Tokens** + +```solidity +function transferFrom(address from, address to, uint256 tokenId) public override { + require(isOwner(tokenId, msg.sender), "MO-NFT: Caller is not an owner"); + require(to != address(0), "MO-NFT: Transfer to zero address"); + require(!isOwner(tokenId, to), "MO-NFT: Recipient is already an owner"); + + // Add the new owner to the set + _owners[tokenId].add(to); + _balances[to] += 1; + + // Transfer the transferValue to the provider + uint256 transferValue = _transferValues[tokenId]; + address provider = _providers[tokenId]; + require(address(this).balance >= transferValue, "Insufficient contract balance"); + + (bool success, ) = provider.call{value: transferValue}(""); + require(success, "Transfer to provider failed"); + + emit TokenTransferred(tokenId, from, to); +} +``` + +**Burning Ownership** + +```solidity +function burn(uint256 tokenId) external { + require(isOwner(tokenId, msg.sender), "MO-NFT: Caller is not an owner"); + + // Remove the caller from the owners set + _owners[tokenId].remove(msg.sender); + _balances[msg.sender] -= 1; + + emit TokenBurned(tokenId, msg.sender); +} +``` + +## Security Considerations + +1. **Reentrancy Attacks**: + + - **Mitigation**: Use the Checks-Effects-Interactions pattern when transferring Ether (e.g., transferring `transferValue` to the provider). + + - **Recommendation**: Consider using `ReentrancyGuard` from OpenZeppelin to prevent reentrant calls. + +2. **Integer Overflow and Underflow**: + + - **Mitigation**: Solidity 0.8.x automatically checks for overflows and underflows, throwing exceptions when they occur. + +3. **Access Control**: + + - **Ensured By**: + + - Only owners can call transfer functions. + + - Only providers can set the `transferValue`. + + - Use of `require` statements to enforce access control. + +4. **Denial of Service (DoS)**: + + - **Consideration**: Functions that iterate over owners could be expensive in terms of gas if the owners list is large. + + - **Mitigation**: Avoid such functions or limit the number of owners. + +5. **Data Integrity**: + + - **Ensured By**: Proper use of Solidity's data types and structures, and by emitting events for all state-changing operations for off-chain verification. + +6. **Ether Handling**: + + - **Consideration**: Ensure the contract can receive Ether to handle provider payments. + + - **Mitigation**: Implement a `receive()` function to accept Ether. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7694/images/Schema.svg b/assets/erc-7694/images/Schema.svg new file mode 100644 index 0000000000..44e6d5aa58 --- /dev/null +++ b/assets/erc-7694/images/Schema.svg @@ -0,0 +1,1768 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OffchainLookup[sender urls[] calldata callback() extradata ] + callback[] + + + + extradata + + + + + + + + + READ + RETURN + GATEWAY + result + + + EIP-3668 + EIP-7694 + L2 + DATABASE + SOLANA + STORAGE + + + + + + + + + + + L 2 + DATABASE + SOLANA + + STORE + GATEWAY + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + REQUIRES KEYGEN + + REQUIRES SIGNATURE + + + + + + + + + + REQUIRES $ PAYMENT + + + + + + NS1 + NS2 + NS3 + + + + + + + + + + + NS1 + NS2 + NS3 + + + + + + + + + JSON-RPC + HTTP + + API + METADATA + + + + + + + + + + + + + JSON-RPC + + + + IPFS + ARWEAVE + IPNS + AR + IPFS + IPNS + ARNS + IPFS2 + AR-IO + EIP-7700 + EIP-7700 + + + + \ No newline at end of file diff --git a/assets/erc-7694/images/Solana.svg b/assets/erc-7694/images/Solana.svg new file mode 100644 index 0000000000..4e7b1d2133 --- /dev/null +++ b/assets/erc-7694/images/Solana.svg @@ -0,0 +1,648 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Execute on Solana [programId] [account] + + + setValue[calldata] + + + + + L1 + + SOL + + CONTRACT 1 + PROGRAM 2 + + + + + + + + + + + + + + + + revert StorageRoutedToSolana[programId, + account] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + response + + + CLIENT + [callData] + + + + \ No newline at end of file diff --git a/assets/erc-7743/DigitalAsset.sol b/assets/erc-7743/DigitalAsset.sol new file mode 100644 index 0000000000..7f3d6de140 --- /dev/null +++ b/assets/erc-7743/DigitalAsset.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./MONFT.sol"; +import "./IDigitalAsset.sol"; + +contract DigitAsset is MultiOwnerNFT, IDigitalAsset { + struct DigitAssetInfo { + uint256 assetId; + string assetName; + uint256 size; + bytes32 fileHash; + address provider; + uint256 transferValue; + } + + mapping(uint256 => DigitAssetInfo) private _assets; + + // Events + event AssetProvided( + uint256 indexed assetId, + uint256 indexed size, + address indexed provider, + bytes32 fileHash, + uint256 transferValue + ); + event AssetTransferred( + address indexed from, + address indexed to, + uint256 indexed assetId, + uint256 transferValue + ); + event TransferValueUpdated( + uint256 indexed assetId, + uint256 oldTransferValue, + uint256 newTransferValue + ); + + constructor() payable MultiOwnerNFT(msg.sender) {} + + function getAssetsTotalSupply() public view override returns (uint256) { + return totalSupply(); + } + + function getAssetName( + uint256 assetId + ) public view override returns (string memory) { + require(_exists(assetId), "Asset: Asset does not exist"); + return _assets[assetId].assetName; + } + + function getAssetDetails( + uint256 assetId + ) public view override returns (AssetDetails memory) { + require(_exists(assetId), "Asset: Asset does not exist"); + DigitAssetInfo storage asset = _assets[assetId]; + return + AssetDetails({ + assetId: asset.assetId, + assetName: asset.assetName, + size: asset.size, + fileHash: asset.fileHash, + provider: asset.provider, + transferValue: asset.transferValue + }); + } + + function provide( + string memory assetName, + uint256 size, + bytes32 fileHash, + address provider, + uint256 transferValue + ) external returns (uint256) { + uint256 assetId = mintToken(); + _assets[assetId] = DigitAssetInfo({ + assetId: assetId, + assetName: assetName, + size: size, + fileHash: fileHash, + provider: provider, + transferValue: transferValue + }); + emit AssetProvided(assetId, size, provider, fileHash, transferValue); + return assetId; + } + + function setTransferValue( + uint256 assetId, + uint256 newTransferValue + ) external { + require(_exists(assetId), "Asset: Asset does not exist"); + DigitAssetInfo storage asset = _assets[assetId]; + require( + msg.sender == asset.provider, + "Only provider can update transfer value" + ); + + uint256 oldTransferValue = asset.transferValue; + asset.transferValue = newTransferValue; + + emit TransferValueUpdated(assetId, oldTransferValue, newTransferValue); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public override { + require( + isOwner(tokenId, msg.sender), // Ensure that `msg.sender` is an owner + "MO-NFT: Transfer from incorrect account" + ); + require(to != address(0), "MO-NFT: Transfer to the zero address"); + + _transferWithProviderPayment(from, to, tokenId); + } + + function _transferWithProviderPayment( + address from, + address to, + uint256 tokenId + ) internal { + DigitAssetInfo storage asset = _assets[tokenId]; + + // Pay the provider the transferValue for this asset + require( + address(this).balance >= asset.transferValue, + "Insufficient contract balance for provider payment" + ); + payable(asset.provider).transfer(asset.transferValue); + + // Call the internal transfer function in MultiOwnerNFT + _transfer(from, to, tokenId); + + emit AssetTransferred(from, to, tokenId, asset.transferValue); + } + + // Allow the contract to receive ETH to fund transfers + receive() external payable {} +} diff --git a/assets/erc-7743/IDigitalAsset.sol b/assets/erc-7743/IDigitalAsset.sol new file mode 100644 index 0000000000..835746e74a --- /dev/null +++ b/assets/erc-7743/IDigitalAsset.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +interface IDigitalAsset { + function getAssetsTotalSupply() external view returns (uint256); + function getAssetName(uint256 assetId) external view returns (string memory); + function getAssetDetails(uint256 assetId) external view returns (AssetDetails memory); + + // Define the structure here as well if you wish to use it as a return type + struct AssetDetails { + uint256 assetId; + string assetName; + uint256 size; + bytes32 fileHash; + address provider; + uint256 transferValue; + } +} \ No newline at end of file diff --git a/assets/erc-7743/MONFT.sol b/assets/erc-7743/MONFT.sol new file mode 100644 index 0000000000..e952994b1a --- /dev/null +++ b/assets/erc-7743/MONFT.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/Context.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import "@openzeppelin/contracts/utils/Address.sol"; +import "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; // Import EnumerableSet + +contract MultiOwnerNFT is Context, ERC165, IERC721, Ownable { + using Address for address; + using EnumerableSet for EnumerableSet.AddressSet; // Use EnumerableSet for address sets + + uint256 private _nextTokenId; + + // Replace array with EnumerableSet for owners + mapping(uint256 => EnumerableSet.AddressSet) internal _owners; + mapping(uint256 => address) private _tokenApprovals; + mapping(address => mapping(address => bool)) private _operatorApprovals; + mapping(address => uint256) private _balances; + + event TokenMinted(uint256 tokenId, address owner); + event TokenTransferred(uint256 tokenId, address from, address to); + event TokenBurned(uint256 tokenId, address owner); // New burn event + + constructor(address owner) Ownable(owner) {} + + function mintToken() public onlyOwner returns (uint256) { + _nextTokenId++; + + // Add the sender to the set of owners for the new token + _owners[_nextTokenId].add(_msgSender()); + + // Increment the balance of the owner + _balances[_msgSender()] += 1; + + emit TokenMinted(_nextTokenId, _msgSender()); + return _nextTokenId; + } + + function isOwner( + uint256 tokenId, + address account + ) public view returns (bool) { + require(_exists(tokenId), "MO-NFT: Token does not exist"); + + // Check if the account is in the owners set for the token + return _owners[tokenId].contains(account); + } + + function _exists(uint256 tokenId) internal view returns (bool) { + return _owners[tokenId].length() > 0; + } + + // IERC721 Functions + + function totalSupply() public view returns (uint256) { + return _nextTokenId; + } + + function balanceOf( + address owner + ) external view override returns (uint256 balance) { + require( + owner != address(0), + "MO-NFT: Balance query for the zero address" + ); + + // Return the balance from the _balances mapping + return _balances[owner]; + } + + function ownerOf( + uint256 tokenId + ) external view override returns (address owner) { + require(_exists(tokenId), "MO-NFT: Owner query for nonexistent token"); + + // Return the first owner in the set (since this is an EnumerableSet, order is not guaranteed) + return _owners[tokenId].at(0); + } + + function getOwnersCount(uint256 tokenId) public view returns (uint256) { + require(_exists(tokenId), "MO-NFT: Token does not exist"); + + // Return the number of owners for the given tokenId + return EnumerableSet.length(_owners[tokenId]); + } + + function approve(address, uint256) external pure override { + revert("MO-NFT: Approve is forbidden"); + } + + function getApproved(uint256) public pure override returns (address) { + revert("MO-NFT: Approve is forbidden"); + } + + function setApprovalForAll(address, bool) external pure override { + revert("MO-NFT: Approve is forbidden"); + } + + function isApprovedForAll( + address, + address + ) public pure override returns (bool) { + revert("MO-NFT: Approve is forbidden"); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public virtual override { + require( + isOwner(tokenId, _msgSender()), + "MO-NFT: Transfer from incorrect account" + ); + require(to != address(0), "MO-NFT: Transfer to the zero address"); + + _transfer(from, to, tokenId); + } + + function safeTransferFrom(address, address, uint256) public pure override { + revert("MO-NFT: safeTransferFrom is forbidden"); + } + + function safeTransferFrom( + address, + address, + uint256, + bytes memory + ) public pure override { + revert("MO-NFT: safeTransferFrom is forbidden"); + } + + function _transfer(address from, address to, uint256 tokenId) internal { + require( + isOwner(tokenId, from), + "MO-NFT: Transfer from incorrect owner" + ); + require(to != address(0), "MO-NFT: Transfer to the zero address"); + require(!isOwner(tokenId, to), "MO-NFT: Recipient is already an owner"); + + // Add the new owner to the EnumerableSet + _owners[tokenId].add(to); + + // Update balances + _balances[to] += 1; + + emit TokenTransferred(tokenId, from, to); + } + + // New burn function + function burn(uint256 tokenId) external { + require( + isOwner(tokenId, _msgSender()), + "MO-NFT: Only an owner can burn their ownership" + ); + + // Remove the caller from the owners set + _owners[tokenId].remove(_msgSender()); + + // Decrement the balance of the owner + _balances[_msgSender()] -= 1; + + emit TokenBurned(tokenId, _msgSender()); + } + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165, IERC165) returns (bool) { + return + interfaceId == type(IERC721).interfaceId || + super.supportsInterface(interfaceId); + } +}