Skip to content

Commit

Permalink
Add example for new tx type execution
Browse files Browse the repository at this point in the history
  • Loading branch information
rmeissner committed Mar 28, 2021
1 parent c460de8 commit f8eb405
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 18 deletions.
10 changes: 5 additions & 5 deletions src/tasks/interaction/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { HardhatRuntimeEnvironment as HRE } from "hardhat/types";

export const contractFactory = (hre: HRE, contractName: string) => hre.ethers.getContractFactory(contractName);

export const contractInstance = async (hre: HRE, contractName: string) => {
const deployment = await hre.deployments.get(contractName);
export const contractInstance = async (hre: HRE, contractName: string, address?: string) => {
const deploymentAddress = address || (await hre.deployments.get(contractName)).address
const contract = await contractFactory(hre, contractName)
return contract.attach(deployment.address)
return contract.attach(deploymentAddress)
}
export const safeSingleton = async (hre: HRE, l2: boolean) => contractInstance(hre, l2 ? "GnosisSafeL2" : "GnosisSafe")
export const proxyFactory = async (hre: HRE) => contractInstance(hre, "GnosisSafeProxyFactory")
export const safeSingleton = async (hre: HRE, l2: boolean, address?: string) => contractInstance(hre, l2 ? "GnosisSafeL2" : "GnosisSafe", address)
export const proxyFactory = async (hre: HRE, address?: string) => contractInstance(hre, "GnosisSafeProxyFactory", address)
8 changes: 5 additions & 3 deletions src/tasks/interaction/creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { task, types } from "hardhat/config";
import { AddressZero } from "@ethersproject/constants";
import { getAddress } from "@ethersproject/address";
import { calculateProxyAddress } from "../../utils/proxies";
import { safeSingleton, proxyFactory } from "./contracts";
import { safeSingleton, proxyFactory, contractFactory } from "./contracts";

const parseSigners = (rawSigners: string): string[] => {
return rawSigners.split(",").map(address => getAddress(address))
Expand All @@ -14,9 +14,11 @@ task("create-safe", "Deploys and verifies Safe contracts")
.addParam("threshold", "Threshold that should be used", 1, types.int, true)
.addParam("fallback", "Fallback handler address", AddressZero, types.string, true)
.addParam("nonce", "Nonce used with factory", new Date().getTime(), types.int, true)
.addParam("singleton", "Set to overwrite which singleton address to use", undefined, types.string, true)
.addParam("factory", "Set to overwrite which factory address to use", undefined, types.string, true)
.setAction(async (taskArgs, hre) => {
const singleton = await safeSingleton(hre, taskArgs.l2)
const factory = await proxyFactory(hre)
const singleton = await safeSingleton(hre, taskArgs.l2, taskArgs.singleton)
const factory = await proxyFactory(hre, taskArgs.singleton)
const signers: string[] = taskArgs.signers ? parseSigners(taskArgs.signers) : [(await hre.getNamedAccounts()).deployer]
const fallbackHandler = getAddress(taskArgs.fallback)
const setupData = singleton.interface.encodeFunctionData(
Expand Down
124 changes: 124 additions & 0 deletions src/tasks/interaction/execution/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { ethers, Wallet, UnsignedTransaction, utils, BigNumber } from "ethers";
import { arrayify, DataOptions, hexlify, isBytesLike, SignatureLike, splitSignature, stripZeros, } from "@ethersproject/bytes";
import { task, types } from "hardhat/config";
import { contractFactory } from "../contracts";
import * as RLP from "@ethersproject/rlp";

import { Logger } from "@ethersproject/logger";
import { getSingletonAddress } from "../information";
import { buildSafeTransaction, populateExecuteTx, safeApproveHash } from "../../../utils/execution";
const logger = new Logger("0.1.0");

const transactionFields = [
{ name: "nonce", maxLength: 32, numeric: true },
{ name: "gasPrice", maxLength: 32, numeric: true },
{ name: "gasLimit", maxLength: 32, numeric: true },
{ name: "to", length: 20 },
{ name: "value", maxLength: 32, numeric: true },
{ name: "data" },
];

interface AccessListEntry {
address: string,
slots?: string[]
}

type AccessList = AccessListEntry[]

export function serialize(transaction: UnsignedTransaction, accessList: AccessList, signature?: SignatureLike): string {
const raw: Array<Array<any> | string | Uint8Array> = [];

let chainId = 0;
if (transaction.chainId != null) {
// A chainId was provided; if non-zero we'll use EIP-155
chainId = transaction.chainId;

if (typeof (chainId) !== "number") {
logger.throwArgumentError("invalid transaction.chainId", "transaction", transaction);
}

} else if (signature && !isBytesLike(signature) && signature.v && signature.v > 28) {
// No chainId provided, but the signature is signing with EIP-155; derive chainId
chainId = Math.floor((signature.v - 35) / 2);
}
raw.push(stripZeros(arrayify(chainId)));

transactionFields.forEach(function (fieldInfo) {
let value = (<any>transaction)[fieldInfo.name] || ([]);
const options: DataOptions = {};
if (fieldInfo.numeric) { options.hexPad = "left"; }
value = arrayify(hexlify(value, options));

// Fixed-width field
if (fieldInfo.length && value.length !== fieldInfo.length && value.length > 0) {
logger.throwArgumentError("invalid length for " + fieldInfo.name, ("transaction:" + fieldInfo.name), value);
}

// Variable-width (with a maximum)
if (fieldInfo.maxLength) {
value = stripZeros(value);
if (value.length > fieldInfo.maxLength) {
logger.throwArgumentError("invalid length for " + fieldInfo.name, ("transaction:" + fieldInfo.name), value);
}
}

raw.push(hexlify(value));
});
raw.push(accessList.map(entry => [entry.address, entry.slots || []]));

// Requesting an unsigned transation
if (!signature) {
return "0x01" + RLP.encode(raw).slice(2);
}

// The splitSignature will ensure the transaction has a recoveryParam in the
// case that the signTransaction function only adds a v.
const sig = splitSignature(signature);

raw.push(stripZeros(arrayify(sig.recoveryParam)));
raw.push(stripZeros(arrayify(sig.r)));
raw.push(stripZeros(arrayify(sig.s)));

return "0x01" + RLP.encode(raw).slice(2);
}

const Forwarder = new ethers.utils.Interface(['function forward(address payable target) payable public'])

task("execute", "Executes a Safe transaction")
.addParam("address", "Address or ENS name of the Safe to check", undefined, types.string)
.addParam("to", "Address of the target", undefined, types.string)
.addParam("value", "Value in ETH", "0", types.string, true)
.addParam("data", "Data as hex string", "0x", types.string, true)
.addParam("signatures", "Comma seperated list of signatures", undefined, types.string, true)
.addFlag("delegatecall", "Indicator if tx should be executed as a delegatecall")
.setAction(async (taskArgs, hre) => {
const mnemonic = process.env.MNEMONIC
if (!mnemonic) throw Error("No mnemonic provided")
const relayer = Wallet.fromMnemonic(mnemonic)
const safe = (await contractFactory(hre, "GnosisSafe")).attach(taskArgs.address)
const safeAddress = await safe.resolvedAddress
console.log(`Using Safe at ${safeAddress} with ${relayer.address}`)
const threshold = await safe.getThreshold()
const nonce = await safe.nonce()
console.log({ threshold, nonce })
const tx = buildSafeTransaction({ to: taskArgs.to, nonce })
const populatedTx: any = await populateExecuteTx(safe, tx, [ await safeApproveHash(relayer, safe, tx, true) ])
const relayerNonce = await hre.ethers.provider.getTransactionCount(relayer.address)
populatedTx.chainId = hre.ethers.provider.network.chainId
populatedTx.gasLimit = BigNumber.from("1000000")
populatedTx.gasPrice = BigNumber.from("10000000000")
populatedTx.nonce = relayerNonce
console.log({ populatedTx })
const accessList: AccessList = [
{ address: await getSingletonAddress(hre, safe.address) }, // Singleton address
]
const setializedTx = serialize(populatedTx, accessList)
const signature = relayer._signingKey().signDigest(utils.keccak256(setializedTx))
console.log({signature})
const signedTx = serialize(populatedTx, accessList, signature)

populatedTx.hash = utils.keccak256(signedTx)
const hash = await hre.ethers.provider.perform("sendTransaction", { signedTransaction: signedTx });
console.log({hash})
await hre.ethers.provider._wrapTransaction(populatedTx, hash);
});
1 change: 1 addition & 0 deletions src/tasks/interaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import "./creation"
import "./execution"
import "./information"
import "./transactions"
3 changes: 2 additions & 1 deletion src/tasks/interaction/information.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AddressOne } from "../../utils/constants";
import { Contract } from "@ethersproject/contracts";
import { contractFactory } from "./contracts";

const getSingletonAddress = async (hre: HRE, address: string): Promise<string> => {
export const getSingletonAddress = async (hre: HRE, address: string): Promise<string> => {
const result = await hre.ethers.provider.getStorageAt(address, 0)
return getAddress("0x" + result.slice(26))
}
Expand Down Expand Up @@ -33,5 +33,6 @@ task("safe-info", "Returns information about a Safe")
console.log(`Version: ${await safe.VERSION()}`)
console.log(`Owners: ${await safe.getOwners()}`)
console.log(`Threshold: ${await safe.getThreshold()}`)
console.log(`Nonce: ${await safe.nonce()}`)
console.log(`Modules: ${await getModules(hre, safe)}`)
});
33 changes: 24 additions & 9 deletions src/utils/execution.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Contract, Wallet, utils, BigNumber, BigNumberish } from "ethers"
import { Contract, Wallet, utils, BigNumber, BigNumberish, Signer, PopulatedTransaction } from "ethers"
import { TypedDataSigner } from "@ethersproject/abstract-signer";
import { AddressZero } from "@ethersproject/constants";

export const EIP_DOMAIN = {
Expand Down Expand Up @@ -68,31 +69,36 @@ export const calculateSafeMessageHash = (safe: Contract, message: string, chainI
return utils._TypedDataEncoder.hash({ verifyingContract: safe.address, chainId }, EIP712_SAFE_MESSAGE_TYPE, { message })
}

export const safeApproveHash = async (signer: Wallet, safe: Contract, safeTx: SafeTransaction, skipOnChainApproval?: boolean): Promise<SafeSignature> => {
export const safeApproveHash = async (signer: Signer, safe: Contract, safeTx: SafeTransaction, skipOnChainApproval?: boolean): Promise<SafeSignature> => {
if (!skipOnChainApproval) {
if (!signer.provider) throw Error("Provider required for on-chain approval")
const chainId = (await signer.provider.getNetwork()).chainId
const typedDataHash = utils.arrayify(calculateSafeTransactionHash(safe, safeTx, chainId))
const signerSafe = safe.connect(signer)
await signerSafe.approveHash(typedDataHash)
}
const signerAddress = await signer.getAddress()
return {
signer: signer.address,
data: "0x000000000000000000000000" + signer.address.slice(2) + "0000000000000000000000000000000000000000000000000000000000000000" + "01"
signer: signerAddress,
data: "0x000000000000000000000000" + signerAddress.slice(2) + "0000000000000000000000000000000000000000000000000000000000000000" + "01"
}
}

export const safeSignTypedData = async (signer: Wallet, safe: Contract, safeTx: SafeTransaction, chainId?: BigNumberish): Promise<SafeSignature> => {
const cid = chainId || (await signer.provider.getNetwork()).chainId
export const safeSignTypedData = async (signer: Signer & TypedDataSigner, safe: Contract, safeTx: SafeTransaction, chainId?: BigNumberish): Promise<SafeSignature> => {
if (!chainId && !signer.provider) throw Error("Provider required to retrieve chainId")
const cid = chainId || (await signer.provider!!.getNetwork()).chainId
const signerAddress = await signer.getAddress()
return {
signer: signer.address,
signer: signerAddress,
data: await signer._signTypedData({ verifyingContract: safe.address, chainId: cid }, EIP712_SAFE_TX_TYPE, safeTx)
}
}

export const signHash = async (signer: Wallet, hash: string): Promise<SafeSignature> => {
export const signHash = async (signer: Signer, hash: string): Promise<SafeSignature> => {
const typedDataHash = utils.arrayify(hash)
const signerAddress = await signer.getAddress()
return {
signer: signer.address,
signer: signerAddress,
data: (await signer.signMessage(typedDataHash)).replace(/1b$/, "1f").replace(/1c$/, "20")
}
}
Expand Down Expand Up @@ -124,6 +130,15 @@ export const executeTx = async (safe: Contract, safeTx: SafeTransaction, signatu
return safe.execTransaction(safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas, safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver, signatureBytes, overrides || {})
}

export const populateExecuteTx = async (safe: Contract, safeTx: SafeTransaction, signatures: SafeSignature[], overrides?: any): Promise<PopulatedTransaction> => {
const signatureBytes = buildSignatureBytes(signatures)
return safe.populateTransaction.execTransaction(
safeTx.to, safeTx.value, safeTx.data, safeTx.operation, safeTx.safeTxGas, safeTx.baseGas, safeTx.gasPrice, safeTx.gasToken, safeTx.refundReceiver,
signatureBytes,
overrides || {}
)
}

export const buildContractCall = (contract: Contract, method: string, params: any[], nonce: number, delegateCall?: boolean, overrides?: Partial<SafeTransaction>): SafeTransaction => {
const data = contract.interface.encodeFunctionData(method, params)
return buildSafeTransaction(Object.assign({
Expand Down

0 comments on commit f8eb405

Please sign in to comment.