Skip to content

Commit

Permalink
Update EIP-7015: allow arbitratry TYPEHASH and correponding fields …
Browse files Browse the repository at this point in the history
…for `structHash`

Merged by EIP-Bot.
  • Loading branch information
oveddan authored Oct 17, 2023
1 parent a33ae85 commit 1051edb
Show file tree
Hide file tree
Showing 6 changed files with 803 additions and 75 deletions.
136 changes: 61 additions & 75 deletions EIPS/eip-7015.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,26 @@ This EIP takes advantage of the fact that contract addresses can be precomputed

**Signing Mechanism**

Creator consent is given by signing an [EIP-712](./eip-712.md) compatible message; all signatures compliant with this EIP MUST include all fields defined. The struct signed is:
Creator consent is given by signing an [EIP-712](./eip-712.md) compatible message; all signatures compliant with this EIP MUST include all fields defined. The struct signed can be any arbitrary data that defines how to create the token; it must hashed in an EIP-712 compatible format with a proper EIP-712 domain.

The following shows some examples of structs that could be encoded into `structHash` (defined below):

```solidity
// example struct that can be encoded in `structHash`; defines that a token can be created with a metadataUri and price:
struct TokenCreation {
bytes32 structHash;
string metadataUri;
uint256 price;
uint256 nonce;
}
```

Where `structHash` represent the hashed data used to deploy the NFT.

**Signature Validation**

Creator attribution is given through a signature verification that MUST be verified by the NFT contract being deployed and an event that MUST be emitted by the NFT contract during the deployment transaction. The event includes all the necessary fields for reconstructing the signed digest and validating the signature to ensure it matches the specified creator. The event name is `CreatorAttribution` and includes the following fields:

- `structHash`: hashed information for deploying the NFT contract (e.g. name, symbol, admins etc)
- `domainName`: the domain name of the contract verifying the singature (for EIP-712 signature validation)
- `structHash`: hashed information for deploying the NFT contract (e.g. name, symbol, admins etc). This corresponds to the value `hashStruct` as defined in the [EIP-712 definition of hashStruct](./eip-712.md#definition-of-hashstruct) standard.
- `domainName`: the domain name of the contract verifying the singature (for EIP-712 signature validation).
- `version`: the version of the contract verifying the signature (for EIP-712 signature validation)
- `creator`: the creator's account
- `signature`: the creator’s signature
Expand All @@ -56,7 +60,7 @@ event CreatorAttribution(
bytes32 structHash,
string domainName,
string version,
address creator,
address creator,
bytes signature
);
```
Expand All @@ -70,78 +74,60 @@ A platform can verify the validity of the creator attribution by reconstructing
#### Example signature validator

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.19;
import "openzeppelin-contracts/contracts/utils/cryptography/EIP712.sol";
import "openzeppelin-contracts/contracts/interfaces/IERC1271.sol";
pragma solidity 0.8.20;
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";
abstract contract ERC7015 is EIP712 {
error Invalid_Signature();
event CreatorAttribution(
bytes32 structHash,
string domainName,
string version,
address creator,
bytes signature
);
/// @notice Define magic value to verify smart contract signatures (ERC1271).
bytes4 internal constant MAGIC_VALUE =
bytes4(keccak256("isValidSignature(bytes32,bytes)"));
bytes32 public constant TYPEHASH =
keccak256(
"CreatorAttribution(bytes32 structHash)"
);
constructor() EIP712("ERC7015", "1") {}
function _validateSignature(
string memory name,
string memory symbol,
bytes32 structHash,
address creator,
bytes memory signature
) internal {
if (!_isValid(structHash, creator, signature))
revert Invalid_Signature();
emit CreatorAttribution(
structHash,
"ERC7015",
"1",
creator,
signature
);
}
error Invalid_Signature();
event CreatorAttribution(
bytes32 structHash,
string domainName,
string version,
address creator,
bytes signature
);
/// @notice Define magic value to verify smart contract signatures (ERC1271).
bytes4 internal constant MAGIC_VALUE =
bytes4(keccak256("isValidSignature(bytes32,bytes)"));
function _isValid(
bytes32 structHash,
address signer,
bytes memory signature
) internal view returns (bool) {
require(signer != address(0), "cannot validate");
bytes32 digest = _hashTypedDataV4(
keccak256(abi.encode(TYPEHASH, structHash, token))
);
if (signer.code.length != 0) {
try IERC1271(signer).isValidSignature(digest, signature) returns (
bytes4 magicValue
) {
return MAGIC_VALUE == magicValue;
} catch {
return false;
}
}
address recoveredSigner = ECDSA.recover(digest, signature);
return recoveredSigner == signer;
function _validateSignature(
bytes32 structHash,
address creator,
bytes memory signature
) internal {
if (!_isValid(structHash, creator, signature)) revert Invalid_Signature();
emit CreatorAttribution(structHash, "ERC7015", "1", creator, signature);
}
function _isValid(
bytes32 structHash,
address signer,
bytes memory signature
) internal view returns (bool) {
require(signer != address(0), "cannot validate");
bytes32 digest = _hashTypedDataV4(structHash);
// if smart contract is the signer, verify using ERC-1271 smart-contract
/// signature verification method
if (signer.code.length != 0) {
try IERC1271(signer).isValidSignature(digest, signature) returns (
bytes4 magicValue
) {
return MAGIC_VALUE == magicValue;
} catch {
return false;
}
}
// otherwise, recover signer and validate that it matches the expected
// signer
address recoveredSigner = ECDSA.recover(digest, signature);
return recoveredSigner == signer;
}
}
```

Expand Down
70 changes: 70 additions & 0 deletions assets/eip-7015/contracts/DelegatedErc721.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

import "./EIP7015.sol";

contract DelegatedErc721 is ERC7015, ERC721, ERC721URIStorage, Ownable {
error AlreadyMinted();
error NotAuthorized();

uint256 private _nextTokenId;

bytes32 public constant TYPEHASH =
keccak256("CreatorAttribution(string uri,uint256 nonce)");

// mapping of signature nonce to if it has been minted
mapping(uint256 => bool) public minted;

constructor(
address initialOwner
) EIP712("ERC7015", "1") ERC721("My Token", "TKN") Ownable(initialOwner) {}

function delegatedSafeMint(
address to,
string memory uri,
uint256 nonce,
address creator,
bytes calldata signature
) external {
uint256 tokenId = _nextTokenId++;

if (!isAuthorizedToCreate(creator)) revert NotAuthorized();

// validate that the nonce has not been used
if (minted[nonce]) revert AlreadyMinted();
minted[nonce] = true;

bytes32 structHash = keccak256(
abi.encode(TYPEHASH, keccak256(bytes(uri)), nonce)
);

_validateSignature(structHash, creator, signature);

_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}

// override required function to define if a signer is authorized to create
function isAuthorizedToCreate(address signer) internal view returns (bool) {
return signer == owner();
}

// The following functions are overrides required by Solidity.
function tokenURI(
uint256 tokenId
) public view override(ERC721, ERC721URIStorage) returns (string memory) {
return super.tokenURI(tokenId);
}

function supportsInterface(
bytes4 interfaceId
) public view override(ERC721, ERC721URIStorage) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
56 changes: 56 additions & 0 deletions assets/eip-7015/contracts/EIP7015.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// SPDX-License-Identifier: CC0-1.0
pragma solidity 0.8.20;
import "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/interfaces/IERC1271.sol";

abstract contract ERC7015 is EIP712 {
error Invalid_Signature();
event CreatorAttribution(
bytes32 structHash,
string domainName,
string version,
address creator,
bytes signature
);

/// @notice Define magic value to verify smart contract signatures (ERC1271).
bytes4 internal constant MAGIC_VALUE =
bytes4(keccak256("isValidSignature(bytes32,bytes)"));

function _validateSignature(
bytes32 structHash,
address creator,
bytes memory signature
) internal {
if (!_isValid(structHash, creator, signature)) revert Invalid_Signature();
emit CreatorAttribution(structHash, "ERC7015", "1", creator, signature);
}

function _isValid(
bytes32 structHash,
address signer,
bytes memory signature
) internal view returns (bool) {
require(signer != address(0), "cannot validate");

bytes32 digest = _hashTypedDataV4(structHash);

// if smart contract is the signer, verify using ERC-1271 smart-contract
/// signature verification method
if (signer.code.length != 0) {
try IERC1271(signer).isValidSignature(digest, signature) returns (
bytes4 magicValue
) {
return MAGIC_VALUE == magicValue;
} catch {
return false;
}
}

// otherwise, recover signer and validate that it matches the expected
// signer
address recoveredSigner = ECDSA.recover(digest, signature);
return recoveredSigner == signer;
}
}
Loading

0 comments on commit 1051edb

Please sign in to comment.