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

Typed and magic signatures guide #292

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 329 additions & 0 deletions docs/pages/guides/typed-on-chain-signatures.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
---
title: EIP712 and EIP1271 Signatures Enabling Message Parameter Verification and Signature Validation
description: How to perform signature verification of messages on-chain and for non-deployed wallet contracts to ensure the integrity of data on a blockchain from signers
---

import {Callout} from 'vocs/components'

# Verified Typed Data & Validated Signature Messages On-chain

Time to complete: 10-20 minutes

In this guide we'll walk you through how to use provided source code for an EVM based contract and explain the inner workings of a custom on-chain message verifier contract that composes the Universal Signature Validator ([ERC-6492](https://ercs.ethereum.org/ERCS/erc-6492)), and how to perform in-app signature verification with typed data.

This can be accomplished with 6 steps:

1. [Create a Builder Project & Obtain an Access Key](/guides/typed-on-chain-signatures#1-create-a-builder-project--obtain-an-access-key)
2. [Initialize React Vite Application](/guides/typed-on-chain-signatures#2-initialize-react-vite-application)
3. [Use Sequence Wallet for User Sign in](/guides/typed-on-chain-signatures#3-use-sequence-wallet-for-user-sign-in)
4. [Use EIP712 Typed Data to Generate EIP6492 Signatures](/guides/typed-on-chain-signatures#4-use-eip712-typed-data-to-generate-eip6492-signatures)
5. [Deploy Contract for EIP712 Verification and EIP1271 Validation](/guides/typed-on-chain-signatures#5-deploy-contract-for-eip712-verification-and-eip1271-validation)
6. [Render Response from Verifying & Validating Contract](/guides/typed-on-chain-signatures#6-render-response-from-verifying--validating-contract)

The general flow for this application can be seen in the following sequence diagram:

![sequence flow for eip712 eip1271 verfying and validating signatures](/img/guides/eip712-eip1271-signatures/eip712-eip1271-signatures.png)

<Callout type="info">
See the [full code for the demo](https://github.com/0xsequence-demos/demo-EIP712-EIP1271-signatures) for more information and an example [demo can be found here](https://demo-eip712-eip1271-signatures.pages.dev/).
</Callout>

## 1. Create a Builder Project & Obtain an Access Key

First, follow [this guide](/solutions/builder/getting-started) to create a project in the Sequence Builder and obtain a project access key.

## 2. Initialize React Vite Application

Next, begin by initializing a new project that will hold all the code neccessary to generate signatures and validation responses from the blockchain:
moskalyk marked this conversation as resolved.
Show resolved Hide resolved

```shell
pnpm create vite
```

This should create a blank project that you can start adding elements and logic to.

## 3. Use Sequence Wallet for User Sign in

Install the necessary packages required for the project to function:

```shell
pnpm install 0xsequence ethers
```

Then enable a user to sign in with on your chosen network and the obtained project access key from [step 1](/guides/typed-on-chain-signatures#1-create-a-builder-project--obtain-an-access-key).

```typescript

import { sequence } from '0xsequence'

function App() {
sequence.initWallet(PROJECT_ACCESS_KEY, {
defaultNetwork: 'sepolia',
});

const signIn = async () => {
const wallet = sequence.getWallet()
const details = await wallet.connect({app: 'sequence signature validation demo'})

if(details){
console.log('is signed in')
console.log(details)
}
}

return (
...
<button onClick={() => signIn()}>sign in</button>
...
)
}
```

## 4. Use EIP712 Typed Data to Generate EIP6492 Signatures

Next, we'll define a custom typed data in typescript and using the utilities library from Sequence constructing a `TypedData` type, where we will be verifying a message structure with `name`, `wallet`, and `message` parameters:

In this example `VERIFYING_CONTRACT_ADDRESS` is the smart contract we deployed on `sepolia` but we will show you in the next step what this contract does so you can deploy yourself any any network:

```typescript
import { sequence } from '0xsequence'

interface Person {
name: string;
wallet: string;
message: string;
}

const VERIFYING_CONTRACT_ADDRESS = '0xB81efF8d6700b83B24AA69ABB18Ca8f9F7A356c5'
const CHAIN_ID = 11155111

const submitSignature = () => {
const wallet = sequence.getWallet()

const message = 'hey' // message can be dynamic
const person: Person = {
name: "user", // name can be dynamic
wallet: wallet.getAddress(),
message: message,
};

const chainId = CHAIN_ID
const typedData: sequence.utils.TypedData = {
domain: {
// Domain settings must match verifying contract
name: "Sequence Signature Validation Demo",
version: "1",
chainId,
verifyingContract: VERIFYING_CONTRACT_ADDRESS,
},
types: {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" },
{ name: "message", type: "string" },
],
},
message: person,
primaryType: "Person",
};
...
}

```

Then we will sign the type message object with the various referenced properties:

```typescript
const wallet = await sequence.getWallet()
const signer = wallet.getSigner(CHAIN_ID);

const signature = await signer.signTypedData(
typedData.domain,
typedData.types,
typedData.message,
{
chainId,
moskalyk marked this conversation as resolved.
Show resolved Hide resolved
eip6492: true, // enabling signatures for non-deployed wallet contracts
}
);

console.log("signature", signature);
```

Great, attach the function to a button and see the signature be generated after a user has clicked the button:

```typescript
<button onClick={() => submitSignature()}>verify signature</button>
```

## 5. Deploy Contract for EIP712 Verification and EIP1271 Validation

We will now provide source code that you can use in something like [Remix](https://remix.ethereum.org/) to deploy a contract, or even something like [Foundry](https://book.getfoundry.sh/reference/forge/forge) for building and deploying with the [Sequence Builder](https://sequence.build)

::::steps

### Universal Signature Validator

The Universal Signature Validator can hypothetically be deployed once for a specific network, and shared with many applications, making it composable and reusable. It's use is for both off-chain and on-chain smart contract wallets for [EIP6492](https://eips.ethereum.org/EIPS/eip-6492) enabled wallets.

You can find [the source code here](https://github.com/0xsequence-demos/EIP712-EIP1271-signatures-demo/blob/master/contracts/src/UniversalSignatureValidator.sol) that you can use to deploy.

### Custom Contract Verifier

The next contract we'll explain more in-depth with the various functions, as this contract can be customized for the specific application. Begin with the following basics with the Universal Signature Validator passed in the constructor in the first step:

```javascript
import {IERC1271} from "./interfaces/IERC1271.sol";
import {IERC6492} from "./interfaces/IERC6492.sol";

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";

struct Person { // can be customized
string name;
address wallet;
string message;
}

contract EIP712Verifier is EIP712 {
moskalyk marked this conversation as resolved.
Show resolved Hide resolved
using ECDSA for bytes32;

IERC6492 public immutable ERC6492_SIGNATURE_VALIDATOR; // the universal signature validator

bytes32 private constant _ERC6492_DETECTION_SUFFIX = 0x6492649264926492649264926492649264926492649264926492649264926492;

// this line of code must be customized to the struct you're verifying
bytes32 private constant _PERSON_TYPEHASH = keccak256(bytes("Person(string name,address wallet,string message)"));

constructor(address erc6492SignatureValidator) {
ERC6492_SIGNATURE_VALIDATOR = IERC6492(erc6492SignatureValidator);
}
...
}

```
### Verify Signature

Next, we have the verify signature function which both: creates the message hash digest and validates the signer:
moskalyk marked this conversation as resolved.
Show resolved Hide resolved

```javascript
/// @dev Verifies the signature of a person.
function verifySignature(address signer, Person memory person, bytes calldata signature)
external
returns (bool success)
{
bytes32 digest = personDigest(person);
return ERC6492_SIGNATURE_VALIDATOR.isValidSig(signer, digest, signature);
}
```

#### Custom Person Digest

In the following function we recreate the struct hash with the passed in parameters, which can be extended to include more or less parameters with varying types:

For more information on constructing the digest, see the [EIP712 specification](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md).

```javascript
/// @dev Returns the EIP712 hash of a person.
function personDigest(Person memory person) public view returns (bytes32 digest) {
bytes32 structHash = keccak256(
abi.encode(_PERSON_TYPEHASH, keccak256(bytes(person.name)), person.wallet, keccak256(bytes(person.message)))
);
digest = EIP712._hashTypedDataV4(structHash);
ScreamingHawk marked this conversation as resolved.
Show resolved Hide resolved
}
```

#### Validate Signer

Next, we validate the `signer` address, the `digest` and `signature`. If an EIP6492 signature has been supplied we use the [universal signature validator](https://eips.ethereum.org/EIPS/eip-6492), otherwise we check the [EIP1271](https://eips.ethereum.org/EIPS/eip-1271) signature validation directly:

```javascript
/// @dev Validates the ERC1271 signature of a signer.
function validateSigner(address signer, bytes32 digest, bytes calldata signature) internal returns (bool success) {
if (signature.length >= 32) {
bool isCounterfactual =
bytes32(signature[signature.length - 32:signature.length]) == _ERC6492_DETECTION_SUFFIX;
if (isCounterfactual) {
return ERC6492_SIGNATURE_VALIDATOR.isValidSig(signer, digest, signature);
}
}

try IERC1271(signer).isValidSignature(digest, signature) returns (bytes4 magicValue) {
return magicValue == IERC1271.isValidSignature.selector;
} catch {}
return false;
}
```
::::

You're ready to deploy both contracts, make sure to choose your network.

## 6. Render Response from Verifying & Validating Contract

We pass the signatures and call the contract deployed using `ethers` with the passed in `PROJECT_ACCESS_KEY` in the following steps:

::::steps
### Create a Provider

Create a provider using the project access key:

```typescript
import { ethers } from 'ethers'

const CHAIN_HANDLE = 'sepolia'

const provider = new ethers.JsonRpcProvider(
`https://nodes.sequence.app/${CHAIN_HANDLE}/${PROJECT_ACCESS_KEY}`
);
```

### Initialize an Ethers Contract
Import the ABI that was generated from [step 5](/guides/typed-on-chain-signatures#5-deploy-contract-for-eip712-verification-and-eip1271-validation) (or copy [from the git source code](https://github.com/0xsequence-demos/EIP712-EIP1271-signatures-demo/blob/master/src/abi.ts)), include the provider, and input the verifying contract address:

```typescript
import { ABI } from "./abi";

const contract = new ethers.Contract(
VERIFYING_CONTRACT_ADDRESS,
ABI,
provider
);
```

### Static Call the Verify Signature Function

By performing a static call on the function, we simulate the transaction without submitting it on chain. This returns a result specifying if the validation was true or false:

```typescript
const address = await wallet.getAddress()

const person: Person = {
name: "user",
wallet: address,
message: message,
}

const signature = await signer.signTypedData(
typedData.domain,
typedData.types,
typedData.message,
{
chainId,
eip6492: true,
}
);

const result = await contract.verifySignature.staticCall(
address,
person,
signature
);

console.log(`Signature is ${result ? "valid" : "invalid"}`);
return result;
```
::::

## Conclusion

Now that we have message structs being passed to a blockchain and messages verified with their inputs, we can extend the application to a multitude of use cases that ensure users are signing the right information (e.g. permitting to spend ERC20's, perform off-chain bidding, QR code containing signature approved minting, etc.).
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,24 @@ export const sidebar = {

// ]
},
{
text: 'Build On-chain Typed Data & Signature Validation',
collapsed: true,
link: '/guides/typed-on-chain-signatures',
// items: [
// { text: 'Sequence Builder Console Signup & Project Creation', link: '/guides/lootbox-guide#1-builder-console-signup--project-creation' },
// { text: 'Access Key Management', link: '/guides/lootbox-guide#2-access-key-management' },
// { text: 'Sequence Kit integration', link: '/guides/lootbox-guide#3-sequence-kit-integration' },
// { text: 'iframe-to-Dapp Communication', link: '/guides/lootbox-guide#4-iframe-to-dapp-communication' },
// { text: 'Deploy a Contract & Sponsor Gas', link: '/guides/lootbox-guide#5-deploy-a-contract--sponsor-gas' },
// { text: 'Deploy a Cloudflare Worker', link: '/guides/lootbox-guide#6-deploy-a-cloudflare-worker' },
// { text: 'Generating AI Prompts & Images', link: '/guides/lootbox-guide#7-generating-ai-prompts--images' },
// { text: 'Store Media To Sequence Metadata Service', link: '/guides/lootbox-guide#8-store-media-to-sequence-metadata-service' },
// { text: 'Securing Your Cloudflare Worker', link: '/guides/lootbox-guide#9-securing-your-cloudflare-worker' },
// { text: '(Optional) Naive Mint Restriction Per Wallet', link: '/guides/lootbox-guide#10-optional-naive-mint-restriction-per-wallet' },

// ]
},
{
text: 'Build a Backend Transaction Service',
collapsed: true,
Expand Down