Skip to content

Commit

Permalink
bugfix: execute safe txs
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoch05 committed Dec 18, 2024
1 parent d72d973 commit 23947ff
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 42 deletions.
52 changes: 52 additions & 0 deletions src/base/safe-service/ceramic.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,56 @@ export class CeramicService extends SafeService {

return Promise.resolve();
}

async getTransactionConfirmations(
safeTxHash: string
): Promise<SafeMultisigConfirmationResponse[]> {
if (!this.composeClient) {
await this.init();
}
try {
const confirmationsIndex: ConfirmationIndexData = (await this
.composeClient.executeQuery(`
query ConfirmationIndex {
confirmationIndex(
first: 99
filters: { where: { transactionHash: { equalTo: "${safeTxHash}" } } }
) {
edges {
node {
owner
id
signature
signatureType
submissionDate
transactionHash
confirmationType
}
}
}
}
`)) as ConfirmationIndexData;
const confirmations = confirmationsIndex.data.confirmationIndex.edges
.map((edge) => edge.node)
.filter((confirmation) => {
const { signature } = confirmation;
let signatureV: number = parseInt(signature.slice(-2), 16);
// must be signed by with prefix, otherwise, we can't verify this message
if (signatureV !== 31 && signatureV !== 32) {
return false;
}
signatureV -= 4;
const normalizedSignature =
signature.slice(0, -2) + signatureV.toString(16);
return (
ethers
.verifyMessage(ethers.getBytes(safeTxHash), normalizedSignature)
.toLowerCase() === confirmation.owner.toLowerCase()
);
});
return confirmations as SafeMultisigConfirmationResponse[];
} catch (error) {
// console.error('Error fetching transaction:', error);
}
}
}
3 changes: 3 additions & 0 deletions src/base/safe-service/safe.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export abstract class SafeService {
this.url = url;
}
abstract proposeTransaction(prop: ProposeTransactionProps): Promise<void>;
abstract getTransactionConfirmations(
safeTxHash: string
): Promise<SafeMultisigConfirmationResponse[]>;
}
8 changes: 8 additions & 0 deletions src/base/safe-service/safeglobal.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@ export class SafeGlobalService extends SafeService {
async proposeTransaction(props: ProposeTransactionProps): Promise<void> {
await this.safeService.proposeTransaction(props);
}


async getTransactionConfirmations(
safeTxHash: string
): Promise<SafeMultisigConfirmationResponse[]> {
const transaction = await this.safeService.getTransaction(safeTxHash);
return transaction?.confirmations;
}
}
18 changes: 18 additions & 0 deletions src/base/safe-service/single.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,22 @@ export class SingleService extends SafeService {
async proposeTransaction(props: ProposeTransactionProps): Promise<void> {
this.props = props;
}

async getTransactionConfirmations(
safeTxHash: string
): Promise<SafeMultisigConfirmationResponse[]> {
if (safeTxHash !== this.props?.safeTxHash) {
return [];
}
return [
{
owner: this.props.senderAddress,
signature: this.props.senderSignature,
signatureType: "ECDSA",
transactionHash: safeTxHash,
submissionDate: new Date().toISOString(),
confirmationType: "approve",
},
];
}
}
123 changes: 101 additions & 22 deletions src/base/safewallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Logger } from "@nestjs/common";
import {
MetaTransactionData,
SafeTransaction,
SafeMultisigConfirmationResponse,
} from "@safe-global/safe-core-sdk-types";
import Safe, { buildSignatureBytes, EthSafeSignature } from "@safe-global/protocol-kit";
import { ethers, Wallet, HDNodeWallet } from "ethers";
Expand All @@ -10,12 +11,20 @@ import { EthereumConnectedWallet } from "./wallet";

export interface TransactionPropose {
readyExecute: boolean;
signedTransaction: SafeTransaction;
safeTransaction: SafeTransaction;
signatures: string | null;
}

export interface SignatureInfo {
size: number;
signatures: string;
selfSigned: boolean;
}

export class SafeWallet {
public address: string;
public wallet: EthereumConnectedWallet;
public owners: string[];
public threshold: number;
private safeSdk: Safe;
private safeService: SafeService;
Expand All @@ -41,9 +50,49 @@ export class SafeWallet {
signer: this.wallet.privateKey,
safeAddress: this.address,
});
this.owners = (await this.safeSdk.getOwners()).map((o) => o.toLowerCase());
this.threshold = await this.safeSdk.getThreshold();
}

private concatSignatures(
confirmations: SafeMultisigConfirmationResponse[]
): SignatureInfo {
// must sort by address
confirmations.sort(
(
left: SafeMultisigConfirmationResponse,
right: SafeMultisigConfirmationResponse
) => {
const leftAddress = left.owner.toLowerCase();
const rightAddress = right.owner.toLowerCase();
if (leftAddress < rightAddress) {
return -1;
} else {
return 1;
}
}
);
var signatures = "0x";
const uniqueOwners = [];
for (const confirmation of confirmations) {
signatures += confirmation.signature.substring(2);
if (
uniqueOwners.includes(confirmation.owner.toLowerCase()) ||
!this.owners.includes(confirmation.owner.toLowerCase())
) {
continue;
}
uniqueOwners.push(confirmation.owner.toLowerCase());
}
return {
size: uniqueOwners.length,
signatures: signatures,
selfSigned: uniqueOwners.includes(
this.wallet.wallet.address.toLowerCase()
),
};
}

async proposeTransaction(
transactions: MetaTransactionData[],
isExecuter: boolean,
Expand All @@ -52,42 +101,72 @@ export class SafeWallet {
this.safeSdk ?? (await this.connect(chainId));
const tx = await this.safeSdk.createTransaction({ transactions });
const txHash = await this.safeSdk.getTransactionHash(tx);
let readyExecute: boolean = false;

if (this.threshold === 1) {
if (isExecuter) {
const signedTransaction = await this.safeSdk.signTransaction(tx);
return {
readyExecute: true,
signedTransaction: signedTransaction,
safeTransaction: tx,
signatures: signedTransaction.encodedSignatures()
};
} else {
return null;
}
} else {
const signedTransaction = await this.safeSdk.signTransaction(tx);
const readyExecute = signedTransaction.signatures.size >= this.threshold;
if (signedTransaction.signatures.size < this.threshold) {
try {
const senderSignature = await this.safeSdk.signHash(txHash)
await this.safeService.proposeTransaction({
safeAddress: this.address,
safeTransactionData: tx.data,
safeTxHash: txHash,
senderAddress: this.wallet.address,
senderSignature: senderSignature.data,
});
this.logger.log(
`finish to propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId}`
);
} catch (err) {
this.logger.warn(
`propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId} failed, err ${err}`
);
let confirmations: SafeMultisigConfirmationResponse[];
try {
confirmations = await this.safeService.getTransactionConfirmations(
txHash
);
} catch {
confirmations = [];
}

var signatureInfo: SignatureInfo = this.concatSignatures(confirmations);
readyExecute = signatureInfo.size >= this.threshold;
if (signatureInfo.selfSigned) {
return {
readyExecute: readyExecute,
safeTransaction: tx,
signatures: signatureInfo.signatures,
};
} else {
if (signatureInfo.size < this.threshold) {
try {
const senderSignature = await this.safeSdk.signHash(txHash)
await this.safeService.proposeTransaction({
safeAddress: this.address,
safeTransactionData: tx.data,
safeTxHash: txHash,
senderAddress: this.wallet.address,
senderSignature: senderSignature.data,
});
signatureInfo.signatures += senderSignature.data.substring(2);
readyExecute = signatureInfo.size + 1 >= this.threshold;
this.logger.log(
`finish to propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId}`
);
} catch (err) {
this.logger.warn(
`propose transaction ${txHash} using ${this.safeService.name} on chain ${chainId} failed, err ${err}`
);
}
}
}
if (!isExecuter) {
return {
readyExecute: false,
safeTransaction: tx,
signatures: signatureInfo.signatures
}
}
// isExecuter
return {
readyExecute: readyExecute,
signedTransaction: signedTransaction
safeTransaction: tx,
signatures: signatureInfo.signatures
};
}
}
Expand Down
40 changes: 20 additions & 20 deletions src/relayer/relayer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,13 +664,13 @@ export class RelayerService implements OnModuleInit {
relayer.safeWallet.address,
relayer.safeWallet.wallet
);
const safeTransactionData = txInfo.signedTransaction.data;
const safeTransaction = txInfo.safeTransaction.data;
const err = await safeContract.tryExecTransaction(
safeTransactionData.to,
safeTransactionData.data,
safeTransaction.to,
safeTransaction.data,
BigInt(0),
safeTransactionData.operation,
txInfo.signedTransaction.encodedSignatures()
safeTransaction.operation,
txInfo.signatures
);
if (err != null) {
this.logger.warn(
Expand All @@ -681,11 +681,11 @@ export class RelayerService implements OnModuleInit {
this.logger.log(`[${chain}] ready to ${hint} using safe tx`);
const gasPrice = await this.gasPrice(chainInfo);
const tx = await safeContract.execTransaction(
safeTransactionData.to,
safeTransactionData.data,
safeTransaction.to,
safeTransaction.data,
BigInt(0),
safeTransactionData.operation,
txInfo.signedTransaction.encodedSignatures(),
safeTransaction.operation,
txInfo.signatures,
gasPrice
);
await this.store.savePendingTransaction(
Expand Down Expand Up @@ -1160,13 +1160,13 @@ export class RelayerService implements OnModuleInit {
bridge.toBridge.safeWallet.address,
bridge.toBridge.safeWallet.wallet
);
const safeTransactionData = txInfo.signedTransaction.data;
const safeTransaction = txInfo.safeTransaction.data;
const err = await safeContract.tryExecTransaction(
safeTransactionData.to,
safeTransactionData.data,
BigInt(safeTransactionData.value),
safeTransactionData.operation,
txInfo.signedTransaction.encodedSignatures(),
safeTransaction.to,
safeTransaction.data,
BigInt(safeTransaction.value),
safeTransaction.operation,
txInfo.signatures,
relayGasLimit
);
if (err != null) {
Expand All @@ -1185,11 +1185,11 @@ export class RelayerService implements OnModuleInit {
)}, gasLimit: ${relayGasLimit}`
);
const tx = await safeContract.execTransaction(
safeTransactionData.to,
safeTransactionData.data,
BigInt(safeTransactionData.value),
safeTransactionData.operation,
txInfo.signedTransaction.encodedSignatures(),
safeTransaction.to,
safeTransaction.data,
BigInt(safeTransaction.value),
safeTransaction.operation,
txInfo.signatures,
gasPrice,
null,
relayGasLimit
Expand Down

0 comments on commit 23947ff

Please sign in to comment.