-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* working forge tests * working rigil signed msg go script * working go script + sol files but w/ static domain hash * clean up and add readme * add git submodule for solmate * remove submodule * remove solmate * add submodule * remove accidental lib * add suave std to remappings * go mod tidy * add args to framework doCall * remove unused code * remove remapping * delete empty lib * forge install: solmate * fix compiler warnings * fix errors * refactor to improve clarity, eip712 example failing * use env config for private key & rpc, clean up * update suave-std * fix lint errors * rename 712/ to crosschain-NFT-mint/ --------- Co-authored-by: dmarzzz <[email protected]> Co-authored-by: Ferran Borreguero <[email protected]>
- Loading branch information
1 parent
db43db1
commit 295816b
Showing
14 changed files
with
560 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {ERC721} from "solmate/tokens/ERC721.sol"; | ||
|
||
/// @title NFTMinter | ||
/// @notice Contract to mint ERC-721 tokens with a signed EIP-712 message | ||
contract SuaveNFT is ERC721 { | ||
// Event declarations | ||
event NFTMintedEvent(address indexed recipient, uint256 indexed tokenId); | ||
|
||
// EIP-712 Domain Separator | ||
// keccak256(abi.encode(keccak256("EIP712Domain(string name,string symbol,uint256 chainId,address verifyingContract)"),keccak256(bytes(NAME)),keccak256(bytes(SYMBOL)),block.chainid,address(this)) | ||
bytes32 public DOMAIN_SEPARATOR = 0x07c5db21fddca4952bc7dee96ea945c5702afed160b9697111b37b16b1289b89; | ||
|
||
// EIP-712 TypeHash | ||
// keccak256("Mint(string name,string symbol,uint256 tokenId,address recipient)"); | ||
bytes32 public constant MINT_TYPEHASH = 0x686aa0ee2a8dd75ace6f66b3a5e79d3dfd8e25e05a5e494bb85e72214ab37880; | ||
|
||
// Authorized signer's address | ||
address public authorizedSigner; | ||
|
||
// NFT Details | ||
string public constant NAME = "SUAVE_NFT"; | ||
string public constant SYMBOL = "NFTEE"; | ||
string public constant TOKEN_URI = "IPFS_URL"; | ||
|
||
constructor(address _authorizedSigner) ERC721(NAME, SYMBOL) { | ||
authorizedSigner = _authorizedSigner; | ||
|
||
// TODO: Make dynamic | ||
// // Initialize DOMAIN_SEPARATOR with EIP-712 domain separator, specific to your contract | ||
// DOMAIN_SEPARATOR = keccak256( | ||
// abi.encode( | ||
// keccak256("EIP712Domain(string name,string symbol,uint256 chainId,address verifyingContract)"), | ||
// keccak256(bytes(NAME)), | ||
// keccak256(bytes(SYMBOL)), | ||
// block.chainid, | ||
// address(this) | ||
// ) | ||
// ); | ||
} | ||
|
||
// Mint NFT with a signed EIP-712 message | ||
function mintNFTWithSignature(uint256 tokenId, address recipient, uint8 v, bytes32 r, bytes32 s) external { | ||
require(verifyEIP712Signature(tokenId, recipient, v, r, s), "INVALID_SIGNATURE"); | ||
|
||
_safeMint(recipient, tokenId); | ||
|
||
emit NFTMintedEvent(recipient, tokenId); | ||
} | ||
|
||
// Verify EIP-712 signature | ||
function verifyEIP712Signature(uint256 tokenId, address recipient, uint8 v, bytes32 r, bytes32 s) | ||
internal | ||
view | ||
returns (bool) | ||
{ | ||
bytes32 digestHash = keccak256( | ||
abi.encodePacked( | ||
"\x19\x01", | ||
DOMAIN_SEPARATOR, | ||
keccak256( | ||
abi.encode(MINT_TYPEHASH, keccak256(bytes(NAME)), keccak256(bytes(SYMBOL)), tokenId, recipient) | ||
) | ||
) | ||
); | ||
|
||
address recovered = ecrecover(digestHash, v, r, s); | ||
|
||
return recovered == authorizedSigner; | ||
} | ||
|
||
// Token URI implementation | ||
function tokenURI(uint256 tokenId) public view override returns (string memory) { | ||
require(_ownerOf[tokenId] != address(0), "NOT_MINTED"); | ||
return TOKEN_URI; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import "forge-std/Test.sol"; | ||
import "../src/NFTEE.sol"; | ||
|
||
contract SuaveNFTTest is Test { | ||
uint256 internal signerPrivateKey; | ||
address internal signerPubKey; | ||
SuaveNFT suaveNFT; | ||
|
||
function setUp() public { | ||
signerPrivateKey = 0xA11CE; | ||
signerPubKey = vm.addr(signerPrivateKey); | ||
suaveNFT = new SuaveNFT(signerPubKey); | ||
} | ||
|
||
function testMintNFTWithSignature() public { | ||
uint256 tokenId = 1; | ||
address recipient = 0xE0f5206BBD039e7b0592d8918820024e2a7437b9; | ||
uint8 v; | ||
bytes32 r; | ||
bytes32 s; | ||
|
||
// Prepare the EIP-712 signature | ||
{ | ||
bytes32 DOMAIN_SEPARATOR = suaveNFT.DOMAIN_SEPARATOR(); | ||
bytes32 structHash = keccak256( | ||
abi.encode( | ||
suaveNFT.MINT_TYPEHASH(), // Use MINT_TYPEHASH from the contract | ||
keccak256(bytes(suaveNFT.NAME())), // Use NAME constant from the contract | ||
keccak256(bytes(suaveNFT.SYMBOL())), // Use SYMBOL constant from the contract | ||
tokenId, | ||
recipient | ||
) | ||
); | ||
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); | ||
// example forge logs for debugging 712 | ||
console.logBytes32(DOMAIN_SEPARATOR); | ||
console.logBytes32(suaveNFT.MINT_TYPEHASH()); | ||
console.logBytes32(keccak256(bytes(suaveNFT.NAME()))); | ||
console.logBytes32(keccak256(bytes(suaveNFT.SYMBOL()))); | ||
console.logBytes32(digest); | ||
|
||
// Sign the digest | ||
(v, r, s) = vm.sign(signerPrivateKey, digest); | ||
} | ||
|
||
// Mint the NFT | ||
suaveNFT.mintNFTWithSignature(tokenId, recipient, v, r, s); | ||
|
||
// Assertions | ||
assertEq(suaveNFT.ownerOf(tokenId), recipient); | ||
assertEq(suaveNFT.tokenURI(tokenId), "IPFS_URL"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# NFTEE - EIP712 Minting Example | ||
|
||
This SUAPP example showcases how you can write a SUAPP to generate a 712 signature on SUAVE that can then ben sent to a contract on Eth L1 which allows you to mint an NFT. | ||
|
||
## Usage | ||
## Solidity | ||
Like all examples in this repo: | ||
```sh | ||
forge build | ||
``` | ||
## Go Script | ||
Before running you need to fill in some values: | ||
- `PRIV_KEY`: Valid ECDSA Private Key with L1 Eth. (Hexadecimal format) | ||
- `ETH_RPC_URL`: Ethereum L1 testnet RPC URL. | ||
- `ETH_CHAIN_ID`: Chain Id of the L1 you're testing on. | ||
|
||
To run the script, execute the following command in your terminal: | ||
|
||
```sh | ||
go run main.go | ||
``` | ||
|
||
## Notes | ||
- The `DOMAIN_SEPARATOR` and `MINT_TYPEHASH` are currently hard coded, you will need to make this dynamic for you prod application. Also Accepting PRs! | ||
- Ensure that the Ethereum Goerli testnet account associated with the provided private key has sufficient ETH to cover transaction fees. | ||
- The script currently targets the Goerli testnet. For mainnet deployment, update the `ETH_RPC_URL` and `ETH_CHAIN_ID` appropriately, and ensure that the account has sufficient mainnet ETH. | ||
|
||
# 712 | ||
The source code for creating the 712 Signature is based off [Testing EIP-712 Signatures](https://book.getfoundry.sh/tutorials/testing-eip712.html). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import "suave-std/suavelib/Suave.sol"; | ||
|
||
contract Emitter { | ||
// Constants matching those in SuaveNFT | ||
string private constant NAME = "SUAVE_NFT"; | ||
string private constant SYMBOL = "NFTEE"; | ||
bytes32 private constant MINT_TYPEHASH = 0x686aa0ee2a8dd75ace6f66b3a5e79d3dfd8e25e05a5e494bb85e72214ab37880; | ||
bytes32 private constant DOMAIN_SEPARATOR = 0x07c5db21fddca4952bc7dee96ea945c5702afed160b9697111b37b16b1289b89; | ||
string private cstoreKey = "NFTEE:v0:PrivateKey"; | ||
|
||
// Private key variable | ||
Suave.DataId public privateKeyDataID; | ||
address public owner; | ||
|
||
// Constructor to initialize owner | ||
constructor() { | ||
owner = msg.sender; | ||
} | ||
|
||
function getPrivateKeyDataIDBytes() public view returns (bytes16) { | ||
return Suave.DataId.unwrap(privateKeyDataID); | ||
} | ||
|
||
// function to fetch private key from confidential input portion of Confidential Compute Request | ||
function fetchConfidentialPrivateKey() public returns (bytes memory) { | ||
require(Suave.isConfidential()); | ||
|
||
bytes memory confidentialInputs = Suave.confidentialInputs(); | ||
return confidentialInputs; | ||
} | ||
|
||
event PrivateKeyUpdateEvent(Suave.DataId dataID); | ||
|
||
// setPrivateKey is the onchain portion of the Confidential Compute Request | ||
// inside we need to store our reference to our private key for future use | ||
// we must do this because updatePrivateKey() is offchain and can't directly store onchain without this | ||
function setPrivateKey(Suave.DataId dataID) public { | ||
privateKeyDataID = dataID; | ||
emit PrivateKeyUpdateEvent(dataID); | ||
} | ||
|
||
// offchain portion of Confidential Compute Request to update privateKey | ||
function updatePrivateKey() public returns (bytes memory) { | ||
require(Suave.isConfidential()); | ||
|
||
bytes memory privateKey = this.fetchConfidentialPrivateKey(); | ||
|
||
// create permissions for data record | ||
address[] memory peekers = new address[](1); | ||
peekers[0] = address(this); | ||
|
||
address[] memory allowedStores = new address[](1); | ||
allowedStores[0] = 0xC8df3686b4Afb2BB53e60EAe97EF043FE03Fb829; // using the wildcard address for allowedStores | ||
|
||
// store private key in conf data store | ||
Suave.DataRecord memory record = Suave.newDataRecord(0, peekers, allowedStores, cstoreKey); | ||
|
||
Suave.confidentialStore(record.id, cstoreKey, privateKey); | ||
|
||
// return calback to emit data ID onchain | ||
return bytes.concat(this.setPrivateKey.selector, abi.encode(record.id)); | ||
} | ||
|
||
event NFTEEApproval(bytes signedMessage); | ||
|
||
function emitSignedMintApproval(bytes memory message) public { | ||
emit NFTEEApproval(message); | ||
} | ||
|
||
// Function to create EIP-712 digest | ||
function createEIP712Digest(uint256 tokenId, address recipient) public view returns (bytes memory) { | ||
require(Suave.DataId.unwrap(privateKeyDataID) != bytes16(0), "private key is not set"); | ||
|
||
bytes32 structHash = | ||
keccak256(abi.encode(MINT_TYPEHASH, keccak256(bytes(NAME)), keccak256(bytes(SYMBOL)), tokenId, recipient)); | ||
bytes32 digestHash = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); | ||
|
||
return abi.encodePacked(digestHash); | ||
} | ||
|
||
// Function to sign and emit a signed EIP 712 digest for minting an NFTEE on L1 | ||
function signL1MintApproval(uint256 tokenId, address recipient) public returns (bytes memory) { | ||
require(Suave.isConfidential()); | ||
require(Suave.DataId.unwrap(privateKeyDataID) != bytes16(0), "private key is not set"); | ||
|
||
bytes memory digest = createEIP712Digest(tokenId, recipient); | ||
|
||
bytes memory signerPrivateKey = Suave.confidentialRetrieve(privateKeyDataID, cstoreKey); | ||
|
||
bytes memory msgBytes = Suave.signMessage(digest, Suave.CryptoSignature.SECP256, string(signerPrivateKey)); | ||
|
||
return bytes.concat(this.emitSignedMintApproval.selector, abi.encode(msgBytes)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import "forge-std/Test.sol"; | ||
import "../src/712Emitter.sol"; | ||
|
||
contract EmitterTest is Test { | ||
Emitter emitter; | ||
address internal owner; | ||
|
||
event NFTEEApproval(bytes signedMessage); | ||
|
||
function setUp() public { | ||
owner = address(this); // Setting the test contract as the owner for testing | ||
emitter = new Emitter(); | ||
} | ||
|
||
function testOwnerInitialization() public { | ||
assertEq(emitter.owner(), owner, "Owner should be initialized correctly"); | ||
} | ||
|
||
function testSetPrivateKey() public { | ||
// Mock DataId and set private key | ||
bytes16 dataIDValue = bytes16(0x1234567890abcdef1234567890abcdef); // Ensure it's 16 bytes | ||
Suave.DataId dataID = Suave.DataId.wrap(dataIDValue); | ||
emitter.setPrivateKey(dataID); | ||
|
||
// Assertion to check if private key was set | ||
// Note: Requires getter for privateKeyDataID or event validation | ||
bytes16 expectedDataIDBytes = Suave.DataId.unwrap(dataID); | ||
bytes16 actualDataIDBytes = emitter.getPrivateKeyDataIDBytes(); | ||
assertEq(actualDataIDBytes, expectedDataIDBytes, "Private key DataID should match"); | ||
} | ||
|
||
function testSignL1MintApproval() public { | ||
// can't actually test this atm | ||
} | ||
|
||
function testEmitSignedMintApproval() public { | ||
bytes memory message = "test message"; | ||
|
||
// Start recording logs | ||
vm.recordLogs(); | ||
|
||
// Call the function to test | ||
emitter.emitSignedMintApproval(message); | ||
|
||
// Get the recorded logs | ||
Vm.Log[] memory logs = vm.getRecordedLogs(); | ||
|
||
// Ensure at least one event was emitted | ||
assert(logs.length > 0); // This line is updated | ||
|
||
// Decode the event data - the structure depends on the event signature | ||
(bytes memory loggedMessage) = abi.decode(logs[0].data, (bytes)); | ||
|
||
// Assertions to validate the event data | ||
assertEq(loggedMessage, message, "Emitted message should match the input message"); | ||
} | ||
} |
Oops, something went wrong.