Skip to content

Commit

Permalink
brock/nftee (#52)
Browse files Browse the repository at this point in the history
* 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
3 people authored Apr 8, 2024
1 parent db43db1 commit 295816b
Show file tree
Hide file tree
Showing 14 changed files with 560 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
[submodule "lib/suave-std"]
path = lib/suave-std
url = https://github.com/flashbots/suave-std

[submodule "lib/solmate"]
path = lib/solmate
url = https://github.com/transmissions11/solmate
6 changes: 4 additions & 2 deletions examples/build-eth-block/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,14 @@ func main() {
allowedPeekers := []common.Address{
buildEthBlockAddress,
bundleContract.Raw().Address(),
ethBlockContract.Raw().Address()}
ethBlockContract.Raw().Address(),
}
allowedStores := []common.Address{}
newBundleArgs := []any{
decryptionCondition,
allowedPeekers,
allowedStores}
allowedStores,
}

confidentialDataBytes, err := bundleContract.Abi.Methods["fetchConfidentialBundleData"].Outputs.Pack(bundleBytes)
maybe(err)
Expand Down
79 changes: 79 additions & 0 deletions examples/crosschain-NFT-mint/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/crosschain-NFT-mint/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/crosschain-NFT-mint/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/crosschain-NFT-mint/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 "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));
}
}
60 changes: 60 additions & 0 deletions examples/crosschain-NFT-mint/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

0 comments on commit 295816b

Please sign in to comment.