Skip to content

Commit

Permalink
Implement EIP-6492 for account and validate
Browse files Browse the repository at this point in the history
  • Loading branch information
Agusx1211 committed Jun 29, 2023
1 parent 616ede2 commit cd5b8eb
Show file tree
Hide file tree
Showing 11 changed files with 683 additions and 26 deletions.
2 changes: 2 additions & 0 deletions packages/abi/src/wallet/erc6492.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

export const abi = [{"inputs":[{"internalType":"bytes","name":"error","type":"bytes"}],"name":"ERC1271Revert","type":"error"},{"inputs":[{"internalType":"bytes","name":"error","type":"bytes"}],"name":"ERC6492DeployFailed","type":"error"},{"inputs":[{"internalType":"address","name":"_signer","type":"address"},{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"}],"name":"isValidSig","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_signer","type":"address"},{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"},{"internalType":"bool","name":"allowSideEffects","type":"bool"},{"internalType":"bool","name":"deployAlreadyDeployed","type":"bool"}],"name":"isValidSigImpl","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_signer","type":"address"},{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"}],"name":"isValidSigNoThrow","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_signer","type":"address"},{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"}],"name":"isValidSigWithSideEffects","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_signer","type":"address"},{"internalType":"bytes32","name":"_hash","type":"bytes32"},{"internalType":"bytes","name":"_signature","type":"bytes"}],"name":"isValidSigWithSideEffectsNoThrow","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}]
2 changes: 2 additions & 0 deletions packages/abi/src/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as erc5719 from './erc5719'
import * as erc1271 from './erc1271'
import * as erc6492 from './erc6492'
import * as factory from './factory'
import * as mainModule from './mainModule'
import * as mainModuleUpgradable from './mainModuleUpgradable'
import * as sequenceUtils from './sequenceUtils'
import * as requireFreshSigner from './libs/requireFreshSigners'

export const walletContracts = {
erc6492,
erc5719,
erc1271,
factory,
Expand Down
63 changes: 59 additions & 4 deletions packages/account/src/account.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { commons, universal } from '@0xsequence/core'
import { EIP_6492_SUFFIX } from '@0xsequence/core/src/commons/validateEIP6492'
import { migrator, defaults, version } from '@0xsequence/migration'
import { NetworkConfig } from '@0xsequence/network'
import { FeeOption, FeeQuote, isRelayer, Relayer, RpcRelayer } from '@0xsequence/relayer'
Expand Down Expand Up @@ -407,7 +408,12 @@ export class Account {
return this.tracker.saveWitnesses({ wallet: this.address, digest, chainId: 0, signatures })
}

async signDigest(digest: ethers.BytesLike, chainId: ethers.BigNumberish, decorate: boolean = true): Promise<string> {
async signDigest(
digest: ethers.BytesLike,
chainId: ethers.BigNumberish,
decorate: boolean = true,
cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore'
): Promise<string> {
// If we are signing a digest for chainId zero then we can never be fully migrated
// because Sequence v1 doesn't allow for signing a message on "all chains"

Expand All @@ -420,10 +426,55 @@ export class Account {

this.mustBeFullyMigrated(status)

// Check if we can validate onchain and what to do if we can't
// revert early, since there is no point in signing a digest now
if (!status.canOnchainValidate && cantValidateBehavior === 'throw') {
throw new Error('Wallet cannot validate onchain')
}

const wallet = this.walletForStatus(chainId, status)
const signature = await wallet.signDigest(digest)

return decorate ? this.decorateSignature(signature, status) : signature
const decorated = decorate ? this.decorateSignature(signature, status) : signature

// If the wallet can't validate onchain then we
// need to prefix the decorated signature with all deployments and migrations
// aka doing a bootstrap using EIP-6492
if (!status.canOnchainValidate) {
switch (cantValidateBehavior) {
// NOTICE: We covered this case before signing the digest
// case 'throw':
// throw new Error('Wallet cannot validate on-chain')
case 'ignore':
return decorated

case 'eip6492':
return this.buildEIP6492Signature(await decorated, status, chainId)
}
}

return decorated
}

private buildEIP6492Signature(
signature: string,
status: AccountStatus,
chainId: ethers.BigNumberish
): string {
const bootstrapBundle = this.buildBootstrapTransactions(status, chainId)
if (bootstrapBundle.transactions.length === 0) {
throw new Error('Cannot build EIP-6492 signature without bootstrap transactions')
}

const encoded = ethers.utils.defaultAbiCoder.encode(
['address', 'bytes', 'bytes'],
[bootstrapBundle.entrypoint, commons.transaction.encodeBundleExecData(bootstrapBundle), signature]
)

return ethers.utils.solidityPack(
['bytes', 'bytes32'],
[encoded, EIP_6492_SUFFIX]
)
}

async editConfig(changes: {
Expand Down Expand Up @@ -536,8 +587,12 @@ export class Account {
return this.relayer(chainId).relay({ ...bootstrapTxs, chainId }, feeQuote)
}

signMessage(message: ethers.BytesLike, chainId: ethers.BigNumberish): Promise<string> {
return this.signDigest(ethers.utils.keccak256(message), chainId)
signMessage(
message: ethers.BytesLike,
chainId: ethers.BigNumberish,
cantValidateBehavior: 'ignore' | 'eip6492' | 'throw' = 'ignore'
): Promise<string> {
return this.signDigest(ethers.utils.keccak256(message), chainId, true, cantValidateBehavior)
}

async signTransactions(
Expand Down
175 changes: 175 additions & 0 deletions packages/account/tests/account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LocalRelayer, Relayer } from '@0xsequence/relayer'
import { commons, v1, v2 } from '@0xsequence/core'
import chaiAsPromised from 'chai-as-promised'
import { Wallet } from '@0xsequence/wallet'
import { encodeBundleExecData } from '@0xsequence/core/src/commons/transaction'

const { expect } = chai.use(chaiAsPromised)

Expand Down Expand Up @@ -222,6 +223,52 @@ describe('Account', () => {
expect(status2.imageHash).to.deep.equal(v2.config.ConfigCoder.imageHashOf(config2))
})

it('Should sign and validate a message without being deployed', async () => {
const signer = randomWallet('Should sign and validate a message without being deployed')
const config = {
threshold: 1,
checkpoint: Math.floor(now() / 1000),
signers: [{ address: signer.address, weight: 1 }]
}

const account = await Account.new({
...defaultArgs,
config,
orchestrator: new Orchestrator([signer]),
})

const msg = ethers.utils.toUtf8Bytes('Hello World')
const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')

const valid = await account.reader(networks[0].chainId).isValidSignature(
account.address,
ethers.utils.keccak256(msg),
sig
)

expect(valid).to.be.true
})

it('Should refuse to sign when not deployed', async () => {
const signer = randomWallet('Should refuse to sign when not deployed')
const config = {
threshold: 1,
checkpoint: Math.floor(now() / 1000),
signers: [{ address: signer.address, weight: 1 }]
}

const account = await Account.new({
...defaultArgs,
config,
orchestrator: new Orchestrator([signer]),
})

const msg = ethers.utils.toUtf8Bytes('Hello World')
const sig = account.signMessage(msg, networks[0].chainId, 'throw')

expect(sig).to.be.rejected
})

describe('After upgrading', () => {
let account: Account

Expand Down Expand Up @@ -914,6 +961,134 @@ describe('Account', () => {
expect(status4.imageHash).to.equal(v2.config.ConfigCoder.imageHashOf(configv2))
expect(status4.version).to.equal(2)
})

context('Signing messages', async () => {
context('After migrating', async () => {
let account: Account
let imageHash: string

beforeEach(async () => {
// Old account may be an address that's not even deployed
const signer1 = randomWallet(
'Signing messages - After migrating' +
account?.address ?? '' // Append prev address to entropy to avoid collisions
)

const simpleConfig = {
threshold: 1,
checkpoint: 0,
signers: [{
address: signer1.address,
weight: 1
}]
}

const config = v1.config.ConfigCoder.fromSimple(simpleConfig)
imageHash = v1.config.ConfigCoder.imageHashOf(config)
const address = commons.context.addressOf(contexts[1], imageHash)

// Sessions server MUST have information about the old wallet
// in production this is retrieved from SequenceUtils contract
await tracker.saveCounterfactualWallet({ config, context: [contexts[1]] })

account = new Account({ ...defaultArgs, address, orchestrator: new Orchestrator([signer1]) })

// Should sign migration using the account
await account.signAllMigrations((c) => c)
})

it('Should validate a message signed by undeployed migrated wallet', async () => {
const msg = ethers.utils.toUtf8Bytes('I like that you are reading our tests')
const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')

const valid = await account.reader(networks[0].chainId).isValidSignature(
account.address,
ethers.utils.keccak256(msg),
sig
)

expect(valid).to.be.true
})

it('Should reject a message signed by undeployed migrated wallet (if set the throw)', async () => {
const msg = ethers.utils.toUtf8Bytes('I do not know what to write here anymore')
const sig = account.signMessage(msg, networks[0].chainId, 'throw')

await expect(sig).to.be.rejected
})

it('Should return an invalid signature by undeployed migrated wallet (if set to ignore)', async () => {
const msg = ethers.utils.toUtf8Bytes('Sending a hug')
const sig = await account.signMessage(msg, networks[0].chainId, 'ignore')

const valid = await account.reader(networks[0].chainId).isValidSignature(
account.address,
ethers.utils.keccak256(msg),
sig
)

expect(valid).to.be.false
})

it('Should validate a message signed by deployed migrated wallet (deployed with v1)', async () => {
const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
await signer1.sendTransaction({
to: deployTx.entrypoint,
data: encodeBundleExecData(deployTx),
})

expect(await networks[0].provider!.getCode(account.address).then((c) => ethers.utils.arrayify(c).length))
.to.not.equal(0)

const msg = ethers.utils.toUtf8Bytes('Everything seems to be working fine so far')
const sig = await account.signMessage(msg, networks[0].chainId, 'eip6492')

const valid = await account.reader(networks[0].chainId).isValidSignature(
account.address,
ethers.utils.keccak256(msg),
sig
)

expect(valid).to.be.true
})

it('Should fail to sign a message signed by deployed migrated wallet (deployed with v1) if throw', async () => {
const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
await signer1.sendTransaction({
to: deployTx.entrypoint,
data: encodeBundleExecData(deployTx),
})

expect(await networks[0].provider!.getCode(account.address).then((c) => ethers.utils.arrayify(c).length))
.to.not.equal(0)

const msg = ethers.utils.toUtf8Bytes('Everything seems to be working fine so far')
const sig = account.signMessage(msg, networks[0].chainId, 'throw')
expect(sig).to.be.rejected
})

it('Should return an invalid signature by deployed migrated wallet (deployed with v1) if ignore', async () => {
const deployTx = Wallet.buildDeployTransaction(contexts[1], imageHash)
await signer1.sendTransaction({
to: deployTx.entrypoint,
data: encodeBundleExecData(deployTx),
})

expect(await networks[0].provider!.getCode(account.address).then((c) => ethers.utils.arrayify(c).length))
.to.not.equal(0)

const msg = ethers.utils.toUtf8Bytes('Everything seems to be working fine so far')
const sig = await account.signMessage(msg, networks[0].chainId, 'ignore')
const valid = await account.reader(networks[0].chainId).isValidSignature(
account.address,
ethers.utils.keccak256(msg),
sig
)

expect(valid).to.be.false
})
})
})
})
})

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/commons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export * as signer from './signer'
export * as EIP1271 from './validateEIP1271'
export * as transaction from './transaction'
export * as reader from './reader'
export * as EIP6492 from './validateEIP6492'

export * from './orchestrator'
27 changes: 5 additions & 22 deletions packages/core/src/commons/reader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { walletContracts } from "@0xsequence/abi"
import { ethers } from "ethers"
import { commons } from ".."
import { isValidCounterfactual } from "./context"
import { validateEIP6492Offchain } from "./validateEIP6492"

/**
* Provides stateful information about the wallet.
Expand Down Expand Up @@ -95,31 +95,14 @@ export interface Reader {
}
}

// We use the EIP-6492 validator contract to check the signature
// this means that if the wallet is not deployed, then the signature
// must be prefixed with a transaction that deploys the wallet
async isValidSignature(
wallet: string,
digest: ethers.BytesLike,
signature: ethers.BytesLike
): Promise<boolean> {
const isDeployed = await this.isDeployed(wallet)

if (isDeployed) {
const isValid = await this.module(wallet).isValidSignature(digest, signature)
return isValid === '0x1626ba7e' // as defined in ERC1271
}

// We can try to recover the counterfactual address
// and check if it matches the wallet address
if (this.contexts) {
return isValidCounterfactual(
wallet,
digest,
signature,
await this.provider.getNetwork().then((n) => n.chainId),
this.provider,
this.contexts
)
} else {
throw new Error('Wallet must be deployed to validate signature, or context info must be provided')
}
return validateEIP6492Offchain(this.provider, wallet, digest, signature)
}
}
Loading

0 comments on commit cd5b8eb

Please sign in to comment.