Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nftee #22

Closed
wants to merge 15 commits into from
1 change: 0 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,3 @@
[submodule "lib/suave-std"]
path = lib/suave-std
url = https://github.com/flashbots/suave-std

79 changes: 79 additions & 0 deletions examples/712/L1/src/NFTEE.sol
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;
}
}
56 changes: 56 additions & 0 deletions examples/712/L1/test/NFTEE.t.sol
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");
}
}
29 changes: 29 additions & 0 deletions examples/712/README.md
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).
97 changes: 97 additions & 0 deletions examples/712/SUAVE/src/712Emitter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "../../../../lib/suave-std/src/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 msg) public {
emit NFTEEApproval(msg);
}

// 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 view 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, string(signerPrivateKey));

return bytes.concat(this.emitSignedMintApproval.selector, abi.encode(msgBytes));
}
}
60 changes: 60 additions & 0 deletions examples/712/SUAVE/test/712Emitter.t.sol
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");
}
}
Loading
Loading