Skip to content

Commit a1aa82a

Browse files
authored
Merge pull request #15 from Layr-Labs/jb/ledger
[1.1.0] ledger<>gnosis rewrite
2 parents 272ab65 + f50f2dd commit a1aa82a

File tree

15 files changed

+3192
-1849
lines changed

15 files changed

+3192
-1849
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11

22
**[Current]**
3+
1.1.0:
4+
- Complete rewrite of the ledger integration, with an emphasis on viem+ledger. Multisig ledger phases now work properly.
5+
36
1.0.6:
47
- disables gnosis verification (cringe)
58

__mocks__/@safe-global/api-kit.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, jest, beforeEach, afterEach } from '@jest/globals';
1+
import { jest } from '@jest/globals';
22
import { SafeMultisigTransactionResponse, SafeMultisigConfirmationResponse } from '@safe-global/types-kit';
33

44
type TGetTransaction = import('@safe-global/api-kit').default['getTransaction']
@@ -13,7 +13,7 @@ const mockConfirmation: () => SafeMultisigConfirmationResponse = () => {
1313
transactionHash: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
1414
confirmationType: "StaticConfirmation",
1515
signature: "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
16-
signatureType: "StaticSignatureType",
16+
signatureType: "ETH_SIGN",
1717
};
1818
}
1919

jest.config.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default {
2626
'!**/node_modules/**',
2727
'!**/vendor/**',
2828
],
29+
extensionsToTreatAsEsm: [".ts", ".tsx"],
2930
coverageReporters: ['html', 'lcov'],
3031
coverageThreshold: {
3132
"global": {

package-lock.json

+2,993-1,729
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@layr-labs/zeus",
3-
"version": "1.0.9",
3+
"version": "1.1.0",
44
"description": "web3 deployer / metadata manager",
55
"main": "src/index.ts",
66
"scripts": {
@@ -32,6 +32,7 @@
3232
"@inquirer/prompts": "^6.0.1",
3333
"@inquirer/search": "^3.0.0",
3434
"@ledgerhq/errors": "^6.19.1",
35+
"@ledgerhq/hw-app-eth": "^6.42.1",
3536
"@ledgerhq/hw-transport-node-hid": "^6.29.5",
3637
"@safe-global/api-kit": "^2.4.6",
3738
"@types/bun": "^1.1.9",
@@ -44,7 +45,6 @@
4445
"cli-spinners": "^3.2.0",
4546
"clipboardy": "^4.0.0",
4647
"cmd-ts": "^0.13.0",
47-
"ethers": "^6.13.3",
4848
"express": "^4.21.0",
4949
"glob": "^11.0.0",
5050
"loading-spinner": "^1.2.1",

src/commands/deploy/cmd/run.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function handler(_user: TState, args: {env: string, resume: boolean
5555
if (!isValidFork(args.fork)) {
5656
throw new Error(`Invalid value for 'fork' - expected one of (tenderly, anvil)`);
5757
}
58-
58+
5959
const user: TLoggedInState = _user;
6060
const repoConfig = await configs.zeus.load();
6161
if (!repoConfig) {

src/commands/prompts.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { select } from './utils';
2-
import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';
2+
import { privateKeyToAccount } from 'viem/accounts';
33
import { search, input, password as inquirerPassword } from '@inquirer/prompts';
44
import chalk from 'chalk';
55
import * as AllChains from "viem/chains";
@@ -125,24 +125,27 @@ export const privateKey: (chainId: number, overridePrompt?: string) => Promise<`
125125
return res as `0x${string}`;
126126
}
127127

128-
export const derivationPath = async () => {
129-
const cont = await wouldYouLikeToContinue("Would you like to use a custom derivation path? (NOTE: the default 'm/44'/60'/0'/0/0' will be used otherwise)");
128+
export const accountIndex = async () => {
129+
const cont = await wouldYouLikeToContinue("Would you like to use a custom bip39 account index? (NOTE: the default 'm/44'/60'/0'/0/[0]s' will be used otherwise)");
130130
if (!cont) {
131-
return false;
131+
return 0;
132132
}
133133

134-
return envVarOrPrompt({
135-
title: `Enter the derivation path (e.g m/44'/60'/0'/0/0)`,
134+
const val = await envVarOrPrompt({
135+
title: `Enter the derivation path suffix (e.g m/44'/60'/0'/0/[0]) - (default: 0)`,
136136
directEntryInputType: 'text',
137-
isValid: (path: string) => {
137+
reuseKey: `derivationPathSuffix`,
138+
isValid: (val: string) => {
138139
try {
139-
mnemonicToAccount('bean spread behind outdoor cotton discover leaf dance captain once intact learn success height cool', {path: path as `m/44'/60'/${string}`})
140+
parseInt(val);
140141
return true;
141142
} catch {
142143
return false;
143144
}
144145
}
145146
})
147+
148+
return parseInt(val);
146149
}
147150

148151
export const signerKey = async (chainId: number, rpcUrl: string, overridePrompt: string | undefined, safeAddress: `0x${string}`) => {
@@ -212,7 +215,7 @@ export const rpcUrl = async (forChainId: number) => {
212215
while (true) {
213216
const result = await envVarOrPrompt({
214217
title: `Enter an RPC url (or $ENV_VAR) for ${chainIdName(forChainId)}`,
215-
reuseKey: `node-${forChainId}`,
218+
reuseKey: `node-url`,
216219
isValid: (text) => {
217220
try {
218221
let url: string = text;

src/deploy/handlers/gnosis.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export async function executeMultisigPhase(deploy: SavebleDocument<TDeploy>, met
4646
if (res.code !== 0) {
4747
throw new HaltDeployError(deploy, `One or more tests failed.`, false);
4848
}
49+
4950
const testOutput = await metatxn.getJSONFile<TTestOutput>(canonicalPaths.testRun({deployEnv: deploy._.env, deployName: deploy._.name, segmentId: deploy._.segmentId}))
5051
testOutput._ = res;
5152
await testOutput.save();

src/signing/strategies/eoa/ledger.ts

+12-23
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,36 @@
11
import EOABaseSigningStrategy from "./eoa";
2-
import { getLedgerSigner } from "../ledgerTransport";
3-
import { JsonRpcProvider } from "ethers";
2+
import { getLedgerAccount } from "../ledgerTransport";
43
import { ICachedArg, TStrategyOptions } from "../../strategy";
54
import { SavebleDocument, Transaction } from "../../../metadata/metadataStore";
65
import { TDeploy } from "../../../metadata/schema";
76
import * as prompts from '../../../commands/prompts';
7+
import { DEFAULT_BASE_DERIVATION_PATH } from "../ledgerTransport";
88

99
export class LedgerSigningStrategy extends EOABaseSigningStrategy {
1010
id = "ledger";
1111
description = "Signing w/ ledger";
1212

13-
public derivationPath: ICachedArg<string | boolean>
13+
public accountIndex: ICachedArg<number>
1414

1515
constructor(deploy: SavebleDocument<TDeploy>, transaction: Transaction, options?: TStrategyOptions) {
1616
super(deploy, transaction, options);
17-
this.derivationPath = this.arg(async () => {
18-
return await prompts.derivationPath();
19-
}, 'derivationPath')
17+
this.accountIndex = this.arg(async () => {
18+
return await prompts.accountIndex();
19+
}, 'accountIndex')
2020
}
2121

2222
async getSignerAddress(): Promise<`0x${string}`> {
2323
console.warn(`If your ledger is not working, you may need to open LedgerLive, navigate to: Accounts -> <Signer> -> Receive and follow the prompts on device. Once your Ledger says "Application is Ready", you can force quit LedgerLive and retry Zeus.`)
24-
const dpArg = await (async () => {
25-
const dp = await this.derivationPath.get();
26-
if (dp !== true && dp !== false) {
27-
return dp
28-
}
29-
})()
30-
31-
const rpc = await this.rpcUrl.get();
32-
const provider = new JsonRpcProvider(rpc);
33-
34-
const signer = await getLedgerSigner(provider, dpArg);
35-
return await signer.getAddress() as `0x${string}`;
24+
const signer = await getLedgerAccount(await this.accountIndex.get());
25+
return await signer.address as `0x${string}`;
3626
}
3727

3828
async subclassForgeArgs(): Promise<string[]> {
3929
const derivationPathArgs = await (async () => {
40-
const dp = await this.derivationPath.get();
41-
if (dp !== true && dp !== false) {
42-
// if a derivation path is specified, use the `--mnemonic-derivation-paths` option.
43-
return [`--mnemonic-derivation-paths`, `${dp}`]
44-
} else {
30+
try {
31+
const accountIndex = await this.accountIndex.getImmediately();
32+
return [`--mnemonic-derivation-paths`, `${DEFAULT_BASE_DERIVATION_PATH}/${accountIndex}`]
33+
} catch {
4534
return [];
4635
}
4736
})()

src/signing/strategies/gnosis/api/gnosisApi.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { MultisigMetadata, TDeploy, TMultisigPhase } from "../../../../metadata/
66
import { overrideTxServiceUrlForChainId } from "./utils";
77
import { TSignatureRequest } from "../../../strategy";
88
import { OperationType } from '@safe-global/types-kit';
9-
import ora from "ora";
109
import chalk from "chalk";
1110
import { getAddress, hexToNumber } from "viem";
1211
import { SafeTransaction } from '@safe-global/types-kit';
@@ -109,6 +108,8 @@ export abstract class GnosisApiStrategy extends GnosisSigningStrategy {
109108
}
110109

111110
const senderSignature = await this.getSignature(version, txn, safeContext.addr)
111+
112+
console.log(`Signature requested: ${senderSignature}`);
112113

113114
await apiKit.proposeTransaction({
114115
safeAddress: getAddress(safeContext.addr),

src/signing/strategies/gnosis/api/gnosisLedger.ts

+34-68
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,40 @@
11
import { GnosisApiStrategy } from "./gnosisApi";
22
import { SafeTransaction } from '@safe-global/types-kit';
33
import { getEip712TxTypes } from "@safe-global/protocol-kit/dist/src/utils/eip-712/index"
4-
import { getDefaultProvider } from 'ethers'
54
import { checkShouldSignGnosisMessage, pressAnyButtonToContinue } from "../../../../commands/prompts";
6-
import { getLedgerSigner } from "../../ledgerTransport";
7-
import { createPublicClient, getContract, http, verifyMessage } from "viem";
5+
import { createPublicClient, getContract, hashTypedData, http } from "viem";
86
import { ICachedArg, TStrategyOptions } from "../../../strategy";
97
import { SavebleDocument, Transaction } from "../../../../metadata/metadataStore";
108
import { TDeploy } from "../../../../metadata/schema";
119
import * as prompts from '../../../../commands/prompts';
12-
import { JsonRpcProvider } from "ethers";
1310
import * as AllChains from 'viem/chains';
1411
import { abi } from "../onchain/Safe";
15-
import { adjustVInSignature, calculateSafeTransactionHash } from "@safe-global/protocol-kit/dist/src/utils";
16-
import { SigningMethod } from "@safe-global/protocol-kit";
12+
import { getLedgerAccount } from "../../ledgerTransport";
1713

1814
export class GnosisLedgerStrategy extends GnosisApiStrategy {
1915
id = "gnosis.api.ledger";
2016
description = "Gnosis API - Ledger (Not For Private Hotfixes)";
2117

22-
public derivationPath: ICachedArg<string | boolean>
18+
public accountIndex: ICachedArg<number>
2319

2420
constructor(deploy: SavebleDocument<TDeploy>, transaction: Transaction, options?: TStrategyOptions) {
2521
super(deploy, transaction, options);
26-
this.derivationPath= this.arg(async () => {
27-
return await prompts.derivationPath();
28-
}, 'derivationPath')
22+
this.accountIndex = this.arg(async () => {
23+
return await prompts.accountIndex();
24+
}, 'accountIndex')
2925
}
30-
31-
async getSignature(version: string, txn: SafeTransaction, safeAddress: `0x${string}`): Promise<`0x${string}`> {
32-
const provider = getDefaultProvider();
3326

34-
const derivationPath = await (async () => {
35-
const dp = await this.derivationPath.get();
36-
if (!dp) return undefined;
37-
if (dp === true) throw new Error(`Invalid.`)
38-
return dp;
39-
})()
27+
async getSignature(version: string, txn: SafeTransaction, safeAddress: `0x${string}`): Promise<`0x${string}`> {
28+
const signer = await getLedgerAccount(await this.accountIndex.get());
29+
if (!signer.signTypedData) {
30+
throw new Error(`This ledger does not support signing typed data, and cannot be used with zeus.`);
31+
}
4032

41-
const signer = await getLedgerSigner(provider, derivationPath);
4233
const types = getEip712TxTypes(version);
43-
const typedDataArgs = {
44-
types: {SafeTx: types.SafeTx},
34+
const typedDataParameters = {
35+
types: types as unknown as Record<string, unknown>,
4536
domain: {
46-
verifyingContract: safeAddress,
37+
verifyingContract: safeAddress as `0x${string}`,
4738
chainId: this.deploy._.chainId,
4839
},
4940
primaryType: 'SafeTx',
@@ -53,42 +44,25 @@ export class GnosisLedgerStrategy extends GnosisApiStrategy {
5344
safeTxGas: txn.data.safeTxGas,
5445
baseGas: txn.data.baseGas,
5546
gasPrice: txn.data.gasPrice,
56-
nonce: txn.data.nonce
47+
nonce: txn.data.nonce,
48+
refundReceiver: txn.data.refundReceiver,
5749
}
58-
} as const;
50+
};
5951

60-
const gnosisHash = calculateSafeTransactionHash(safeAddress, txn.data, version, BigInt(this.deploy._.chainId));
61-
console.log(`Expected gnosis hash: ${gnosisHash}`);
52+
await checkShouldSignGnosisMessage(typedDataParameters);
6253

63-
await checkShouldSignGnosisMessage(typedDataArgs);
54+
if (!signer.signMessage) {
55+
throw new Error(`This ledger doesn't support signing somehow.`);
56+
}
6457

65-
console.log(`Signing with ledger (please check your device for instructions)...`);
58+
const preImage = hashTypedData(typedDataParameters);
59+
console.log(`Signing from: ${signer.address}`);
60+
console.log(`Typed data hash to sign: ${preImage}`);
6661

6762
try {
68-
const addr = await signer.getAddress() as `0x${string}`;
69-
console.log(`The ledger reported this address: ${addr}`);
70-
71-
const _signature = await signer.signMessage(
72-
gnosisHash
73-
) as `0x${string}`
74-
75-
const signature = (await adjustVInSignature(SigningMethod.ETH_SIGN, _signature, gnosisHash, addr)) as `0x${string}`;
76-
77-
const valid = await verifyMessage({address: addr, message: gnosisHash, signature: _signature});
78-
if (!valid) {
79-
console.error(`Failed to verify signature. Nothing will be submitted. (signed from ${addr})`);
80-
console.warn(`Signature: ${_signature}`);
81-
console.log(`V-Adjusted signature: ${signature}`);
82-
console.warn(`Gnosis Hash: ${gnosisHash}`);
83-
console.warn(`From: ${addr}`);
84-
throw new Error(`Invalid signature. Failed to verify typedData.`);
85-
} else {
86-
console.log(`Successfully verified signature (from=${addr},signature=${_signature})`);
87-
}
88-
89-
console.log(`Original Signature: ${_signature}`);
90-
console.log(`V-Adjusted signature: ${signature}`);
91-
63+
console.log(`Signing with ledger(${signer.address}) (please check your device for instructions)...`);
64+
const signature = await signer.signTypedData(typedDataParameters);
65+
console.log(`Signature: ${signature}`);
9266
return signature;
9367
} catch (e) {
9468
if ((e as Error).message.includes(`0x6a80`)) {
@@ -102,22 +76,14 @@ export class GnosisLedgerStrategy extends GnosisApiStrategy {
10276
}
10377

10478
async getSignerAddress(): Promise<`0x${string}`> {
105-
console.log(`Querying ledger for address...`);
106-
107-
const derivationPath = await (async () => {
108-
const dp = await this.derivationPath.get();
109-
if (!dp) return undefined;
110-
if (dp === true) throw new Error(`Invalid.`)
111-
return dp;
112-
})()
79+
console.log(`Querying ledger for address (check device for instructions)...`);
11380

11481
try {
11582
while (true) {
11683
try {
117-
const provider = new JsonRpcProvider(await this.rpcUrl.get());
118-
const signer = await getLedgerSigner(provider, derivationPath);
119-
const res = await signer.getAddress() as `0x${string}`;
120-
console.log(`Detected ledger address(${derivationPath}): ${res}`);
84+
const accountIndex = await this.accountIndex.get();
85+
const signer = await getLedgerAccount(accountIndex);
86+
console.log(`Detected ledger address: ${signer.address}`);
12187

12288
// double check that this ledger is a signer for the multisig.
12389
if (this.forMultisig) {
@@ -130,12 +96,12 @@ export class GnosisLedgerStrategy extends GnosisApiStrategy {
13096
abi,
13197
address: this.forMultisig
13298
})
133-
if (!await safe.read.isOwner([res])) {
134-
throw new Error(`This ledger path (${derivationPath}) produced address (${res}), which is not a signer on the multisig (${this.forMultisig})`);
99+
if (!await safe.read.isOwner([signer.address])) {
100+
throw new Error(`This ledger path (accountIndex=${accountIndex}) produced address (${signer.address}), which is not a signer on the multisig (${this.forMultisig})`);
135101
}
136102
}
137103

138-
return res;
104+
return signer.address;
139105
} catch (e) {
140106
if ((e as Error).message.includes('Locked device')) {
141107
console.error(`Error: Please unlock your ledger.`);

0 commit comments

Comments
 (0)