Skip to content

Commit

Permalink
feat: gnosis safe signer support
Browse files Browse the repository at this point in the history
Allows users to optionally define gnosis safe settings in `hardhat.config.ts`.
If specified, then a gnosis signer will be used.  If not defined, a normal
ethers signer will be used.

Signed-off-by: Ryan Goulding <[email protected]>
  • Loading branch information
ryandgoulding committed Feb 16, 2024
1 parent f0392c5 commit 4215019
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 71 deletions.
1 change: 1 addition & 0 deletions packages/devtools-evm-hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"test": "jest --ci --forceExit"
},
"dependencies": {
"@gnosis.pm/safe-core-sdk": "2.0.0",
"@layerzerolabs/export-deployments": "~0.0.1",
"micro-memoize": "~4.1.2",
"p-memoize": "~4.0.4",
Expand Down
14 changes: 10 additions & 4 deletions packages/devtools-evm-hardhat/src/transactions/signer.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import pMemoize from 'p-memoize'
import type { OmniSignerFactory } from '@layerzerolabs/devtools'
import { OmniSignerEVM } from '@layerzerolabs/devtools-evm'
import { GnosisOmniSignerEVM, OmniSignerEVM } from '@layerzerolabs/devtools-evm'
import { createProviderFactory } from '@/provider'
import { createGetHreByEid } from '@/runtime'

export const createSignerFactory = (
addressOrIndex?: string | number,
providerFactory = createProviderFactory()
): OmniSignerFactory<OmniSignerEVM> => {
providerFactory = createProviderFactory(),
networkEnvironmentFactory = createGetHreByEid()
): OmniSignerFactory => {
return pMemoize(async (eid) => {
const provider = await providerFactory(eid)
const signer = provider.getSigner(addressOrIndex)

return new OmniSignerEVM(eid, signer)
const env = await networkEnvironmentFactory(eid)
const safeConfig = env.network.config.safeConfig
return safeConfig
? new GnosisOmniSignerEVM(eid, signer, safeConfig.safeUrl, safeConfig)
: new OmniSignerEVM(eid, signer)
})
}
21 changes: 19 additions & 2 deletions packages/devtools-evm-hardhat/src/type-extensions.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import 'hardhat/types/config'
import { EndpointId } from '@layerzerolabs/lz-definitions'
import { ConnectSafeConfig } from '@gnosis.pm/safe-core-sdk'

declare module 'hardhat/types/config' {
interface HardhatNetworkUserConfig {
eid?: never
safeConfig?: never
}

interface HardhatNetworkConfig {
eid?: never
safeConfig?: never
}

interface HttpNetworkUserConfig {
Expand All @@ -17,10 +20,15 @@ declare module 'hardhat/types/config' {
* on this network.
*
* This allows you to use arbitrary network names while maintaining
* allowing you to easilty find deployment and artifact information
* allowing you to easily find deployment and artifact information
* for LayerZero protocol contracts using the standard hardhat deploy methods
*/
eid?: EndpointId

/**
* Optional gnosis safe config.
*/
safeConfig?: SafeConfig
}

interface HttpNetworkConfig {
Expand All @@ -30,10 +38,19 @@ declare module 'hardhat/types/config' {
* on this network.
*
* This allows you to use arbitrary network names while maintaining
* allowing you to easilty find deployment and artifact information
* allowing you to easily find deployment and artifact information
* for LayerZero protocol contracts using the standard hardhat deploy methods
*/
eid?: EndpointId

/**
* Optional gnosis safe config.
*/
safeConfig?: SafeConfig
}
interface SafeConfig extends ConnectSafeConfig {
safeUrl: string
safeAddress: string // override to make ConnectSafeConfig.safeAddress mandatory
}

interface HardhatUserConfig {
Expand Down
5 changes: 5 additions & 0 deletions packages/devtools-evm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
"test": "jest --ci"
},
"dependencies": {
"@gnosis.pm/safe-core-sdk": "^2.0.0",
"@gnosis.pm/safe-core-sdk-types": "^1.0.0",
"@gnosis.pm/safe-ethers-lib": "^1.7.0",
"@gnosis.pm/safe-service-client": "1.1.1",
"ethers": "5.7.2",
"p-memoize": "~4.0.4"
},
"devDependencies": {
Expand Down
137 changes: 107 additions & 30 deletions packages/devtools-evm/src/signer/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { TransactionReceipt, TransactionRequest } from '@ethersproject/abstract-provider'
import type { Signer } from '@ethersproject/abstract-signer'
import Safe, { ConnectSafeConfig } from '@gnosis.pm/safe-core-sdk'
import { SafeTransactionDataPartial } from '@gnosis.pm/safe-core-sdk-types'
import EthersAdapter from '@gnosis.pm/safe-ethers-lib'
import SafeServiceClient from '@gnosis.pm/safe-service-client'
import type { EndpointId } from '@layerzerolabs/lz-definitions'
import {
formatEid,
Expand All @@ -10,23 +14,46 @@ import {
} from '@layerzerolabs/devtools'
import assert from 'assert'

import { ethers } from 'ethers'

export abstract class OmniSignerEVMBase implements OmniSigner {
protected constructor(
public readonly eid: EndpointId,
public readonly signer: Signer
) {}

protected assertTransaction(transaction: OmniTransaction) {
assert(
transaction.point.eid === this.eid,
`Could not use signer for ${formatEid(this.eid)} to sign a transaction for ${formatOmniPoint(
transaction.point
)}`
)
}

abstract sign(transaction: OmniTransaction): Promise<string>
abstract signAndSend(transaction: OmniTransaction): Promise<OmniTransactionResponse>
}

/**
* Implements an OmniSigner interface for EVM-compatible chains
*/
export class OmniSignerEVM implements OmniSigner {
export class OmniSignerEVM extends OmniSignerEVMBase {
constructor(
public readonly eid: EndpointId,
public readonly signer: Signer
) {}
public override readonly eid: EndpointId,
public override readonly signer: Signer
) {
super(eid, signer)
}

async sign(transaction: OmniTransaction): Promise<string> {
this.#assertTransaction(transaction)
this.assertTransaction(transaction)

return this.signer.signTransaction(this.#serializeTransaction(transaction))
}

async signAndSend(transaction: OmniTransaction): Promise<OmniTransactionResponse<TransactionReceipt>> {
this.#assertTransaction(transaction)
this.assertTransaction(transaction)

const nativeTransaction = this.#serializeTransaction(transaction)
const { hash, ...response } = await this.signer.sendTransaction(nativeTransaction)
Expand All @@ -37,41 +64,91 @@ export class OmniSignerEVM implements OmniSigner {
}
}

#assertTransaction(transaction: OmniTransaction) {
assert(
transaction.point.eid === this.eid,
`Could not use signer for ${formatEid(this.eid)} to sign a transaction for ${formatOmniPoint(
transaction.point
)}`
)
}

#serializeTransaction(transaction: OmniTransaction): TransactionRequest {
// Still missing
//
return {
// mandatory
to: transaction.point.address,
data: transaction.data,

// from?: string,
// nonce?: BigNumberish,
// optional
...(transaction.gasLimit && { gasLimit: transaction.gasLimit }),
...(transaction.value && { value: transaction.value }),
}
}
}

// gasPrice?: BigNumberish,
/**
* Implements an OmniSigner interface for EVM-compatible chains using Gnosis Safe.
*/
export class GnosisOmniSignerEVM<TSafeConfig extends ConnectSafeConfig> extends OmniSignerEVMBase {
// TODO: upgrade from @gnosis.pm to @safeglobal dependencies once the codebase upgrades to Ethers v6. Currently,
// devtools only supports Ethers v5, and @safeglobal only supports Ethers v6.

// data?: BytesLike,
// chainId?: number
protected safeSdk: Safe | undefined
protected safeService: SafeServiceClient | undefined

// type?: number;
// accessList?: AccessListish;
constructor(
public override readonly eid: EndpointId,
public override readonly signer: Signer,
protected readonly safeUrl: string,
protected readonly safeConfig: TSafeConfig
) {
super(eid, signer)
}

// maxPriorityFeePerGas?: BigNumberish;
// maxFeePerGas?: BigNumberish;
async sign(_transaction: OmniTransaction): Promise<string> {
throw new Error('Method not implemented.')
}

async signAndSend(transaction: OmniTransaction): Promise<OmniTransactionResponse> {
this.assertTransaction(transaction)
const { safeSdk, safeService } = await this.#initSafe()
const safeTransaction = await safeSdk.createTransaction([this.#serializeTransaction(transaction)])
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction)
const safeAddress = safeSdk.getAddress()
const senderAddress = await this.signer.getAddress()
await safeService.proposeTransaction({
safeAddress,
safeTransaction,
safeTxHash,
senderAddress,
})
return {
transactionHash: safeTxHash,
wait: async (_confirmations?: number) => {
return {
transactionHash: safeTxHash,
}
},
}
}

#serializeTransaction(transaction: OmniTransaction): SafeTransactionDataPartial {
return {
// mandatory
to: transaction.point.address,
data: transaction.data,
value: '0',
}
}

// optional
...(transaction.gasLimit && { gasLimit: transaction.gasLimit }),
...(transaction.value && { value: transaction.value }),
async #initSafe() {
if (this.safeConfig && (!this.safeSdk || !this.safeService)) {
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: this.signer,
})
this.safeService = new SafeServiceClient(this.safeUrl)

const contractNetworks = this.safeConfig.contractNetworks
this.safeSdk = await Safe.create({
ethAdapter,
safeAddress: this.safeConfig.safeAddress!,
...(!!contractNetworks && { contractNetworks }),
})
}
if (!this.safeSdk || !this.safeService) {
throw new Error('Safe SDK not initialized')
}
return { safeSdk: this.safeSdk, safeService: this.safeService }
}
}
70 changes: 68 additions & 2 deletions packages/devtools-evm/test/signer/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import fc from 'fast-check'
import { endpointArbitrary, pointArbitrary } from '@layerzerolabs/test-devtools'
import { Signer } from '@ethersproject/abstract-signer'
import { OmniSignerEVM } from '@/signer'
import { GnosisOmniSignerEVM, OmniSignerEVM } from '@/signer'
import Safe, { SafeConfig } from '@gnosis.pm/safe-core-sdk'
import SafeServiceClient from '@gnosis.pm/safe-service-client'

describe('signer/sdk', () => {
describe('signer/ethers', () => {
const transactionHashArbitrary = fc.hexaString()
const signedTransactionArbitrary = fc.hexaString()
const transactionArbitrary = fc.record({
Expand Down Expand Up @@ -78,4 +80,68 @@ describe('signer/sdk', () => {
})
})
})
describe('GnosisOmniSignerEVM', () => {
describe('sign', () => {
it('should throw', async () => {
await fc.assert(
fc.asyncProperty(endpointArbitrary, transactionArbitrary, async (eid, transaction) => {
const signer = {} as Signer
const omniSigner = new GnosisOmniSignerEVM(eid, signer, '', {} as SafeConfig)
await expect(() => omniSigner.sign(transaction)).rejects.toThrow(/Method not implemented/)
})
)
})
})
describe('signAndSend', () => {
it('should reject if the eid of the transaction does not match the eid of the signer', async () => {
await fc.assert(
fc.asyncProperty(endpointArbitrary, transactionArbitrary, async (eid, transaction) => {
fc.pre(eid !== transaction.point.eid)

const signer = {} as Signer
const omniSigner = new GnosisOmniSignerEVM(eid, signer, '', {} as SafeConfig)

await expect(() => omniSigner.signAndSend(transaction)).rejects.toThrow(/Could not use signer/)
})
)
})
it('should send the transaction using the signer if the eids match', async () => {
await fc.assert(
fc.asyncProperty(
transactionArbitrary,
transactionHashArbitrary,
async (transaction, transactionHash) => {
const sendTransaction = jest.fn()
const getAddress = jest.fn()
const signer = { getAddress, sendTransaction } as unknown as Signer
const omniSigner = new GnosisOmniSignerEVM(
transaction.point.eid,
signer,
'',
{} as SafeConfig
)
omniSigner['safeSdk'] = {
createTransaction: jest.fn(),
getTransactionHash: jest.fn().mockResolvedValue(transactionHash),
getAddress: jest.fn(),
} as unknown as Safe
const safeService = (omniSigner['safeService'] = {
proposeTransaction: jest.fn(),
} as unknown as SafeServiceClient)

const result = await omniSigner.signAndSend(transaction)
expect(result.transactionHash).toEqual(transactionHash)
expect(await result.wait()).toEqual({ transactionHash })
expect(safeService.proposeTransaction).toHaveBeenCalledWith({
safeAddress: undefined,
safeTransaction: undefined,
safeTxHash: transactionHash,
senderAddress: undefined,
})
}
)
)
})
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ const action: ActionType<TaskArgs> = async (
`There are ${transactions.length} transactions required to configure the OApp`
)
)

// Now sign & send the transactions
const signAndSendResult = await hre.run(SUBTASK_LZ_SIGN_AND_SEND, {
transactions,
Expand Down
Loading

0 comments on commit 4215019

Please sign in to comment.