mirror of
https://github.com/getwax/wax.git
synced 2026-01-10 15:48:10 -05:00
Use SafeSingletonFactory
This commit is contained in:
@@ -2,12 +2,14 @@
|
||||
|
||||
import { ethers } from 'hardhat';
|
||||
import { FeeMeasurer__factory } from '../typechain-types';
|
||||
import SafeSingletonFactory from '../src/SafeSingletonFactory';
|
||||
|
||||
async function main() {
|
||||
const [signer] = await ethers.getSigners();
|
||||
|
||||
const feeMeasurer = await new FeeMeasurer__factory().connect(signer).deploy();
|
||||
await feeMeasurer.deploymentTransaction()?.wait();
|
||||
const ssf = await SafeSingletonFactory.init(signer);
|
||||
|
||||
const feeMeasurer = await ssf.connectOrDeploy(FeeMeasurer__factory, []);
|
||||
|
||||
console.log('FeeMeasurer deployed to:', await feeMeasurer.getAddress());
|
||||
|
||||
|
||||
419
tools/fee-measurer/src/SafeSingletonFactory.ts
Normal file
419
tools/fee-measurer/src/SafeSingletonFactory.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import { ethers, Signer } from 'ethers';
|
||||
import SignerOrProvider from './SignerOrProvider';
|
||||
import assert from './assert';
|
||||
|
||||
/**
|
||||
* Filters out the optional elements of an array type because an optional
|
||||
* element isn't considered to match First in [infer First, ...].
|
||||
*/
|
||||
type NonOptionalElementsOf<A extends unknown[]> = A extends [
|
||||
infer First,
|
||||
...infer Tail,
|
||||
]
|
||||
? [First, ...NonOptionalElementsOf<Tail>]
|
||||
: A extends [opt?: unknown]
|
||||
? []
|
||||
: never;
|
||||
|
||||
export type ContractFactoryConstructor = {
|
||||
new (): ethers.ContractFactory;
|
||||
connect(
|
||||
address: string,
|
||||
runner?: ethers.ContractRunner | null,
|
||||
): Pick<ethers.Contract, 'getAddress'>;
|
||||
};
|
||||
|
||||
export type DeployParams<CFC extends ContractFactoryConstructor> =
|
||||
NonOptionalElementsOf<Parameters<InstanceType<CFC>['deploy']>>;
|
||||
|
||||
type Deployment = {
|
||||
gasPrice: bigint;
|
||||
gasLimit: bigint;
|
||||
signerAddress: string;
|
||||
transaction: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export default class SafeSingletonFactory {
|
||||
static sharedAddress = '0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7';
|
||||
|
||||
static deployments: Record<number, Deployment | undefined> = {
|
||||
1337: {
|
||||
gasPrice: 100000000000n,
|
||||
gasLimit: 100000n,
|
||||
signerAddress: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37',
|
||||
transaction: [
|
||||
'0x',
|
||||
'f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff',
|
||||
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081',
|
||||
'602082378035828234f58015156039578182fd5b8082525050506014600cf3820a96a',
|
||||
'0460c6ea9b8f791e5d9e67fbf2c70aba92bf88591c39ac3747ea1bedc2ef1750ca04b',
|
||||
'08a4b5cea15a56276513da7a0c0b34f16e89811d5dd911efba5f8625a921cc',
|
||||
].join(''),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
31337: {
|
||||
gasPrice: 100000000000n,
|
||||
gasLimit: 100000n,
|
||||
signerAddress: '0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37',
|
||||
transaction: [
|
||||
'0x',
|
||||
'f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff',
|
||||
'fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081',
|
||||
'602082378035828234f58015156039578182fd5b8082525050506014600cf382f4f5a',
|
||||
'00dc4d1d21b308094a30f5f93da35e4d72e99115378f135f2295bea47301a3165a063',
|
||||
'6b822daad40aa8c52dd5132f378c0c0e6d83b4898228c7e21c84e631a0b891',
|
||||
].join(''),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
};
|
||||
|
||||
provider: ethers.Provider;
|
||||
viewer: SafeSingletonFactoryViewer;
|
||||
|
||||
private constructor(
|
||||
public signer: ethers.Signer,
|
||||
public chainId: bigint,
|
||||
public address: string,
|
||||
) {
|
||||
if (!signer.provider) {
|
||||
throw new Error('Expected signer with provider');
|
||||
}
|
||||
|
||||
this.provider = signer.provider;
|
||||
|
||||
this.viewer = new SafeSingletonFactoryViewer(signer, chainId);
|
||||
}
|
||||
|
||||
static async init(signer: ethers.Signer): Promise<SafeSingletonFactory> {
|
||||
const { provider } = signer;
|
||||
assert(provider, 'Expected signer with provider');
|
||||
|
||||
const { chainId } = await provider.getNetwork();
|
||||
|
||||
const address =
|
||||
SafeSingletonFactory.deployments[Number(chainId)]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
const existingCode = await provider.getCode(address);
|
||||
|
||||
if (existingCode !== '0x') {
|
||||
return new SafeSingletonFactory(signer, chainId, address);
|
||||
}
|
||||
|
||||
const deployment = SafeSingletonFactory.deployments[Number(chainId)];
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(
|
||||
[
|
||||
'Cannot get deployment for SafeSingletonFactory (check',
|
||||
'https://github.com/safe-global/safe-singleton-factory/tree/main/artifacts',
|
||||
`for chain id ${chainId})`,
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
// Fund the eoa account for the presigned transaction
|
||||
await (
|
||||
await signer.sendTransaction({
|
||||
to: deployment.signerAddress,
|
||||
value: BigInt(deployment.gasPrice) * BigInt(deployment.gasLimit),
|
||||
})
|
||||
).wait();
|
||||
|
||||
await (await provider.broadcastTransaction(deployment.transaction)).wait();
|
||||
|
||||
const deployedCode = await provider.getCode(deployment.address);
|
||||
assert(deployedCode !== '0x', 'Failed to deploy safe singleton factory');
|
||||
|
||||
return new SafeSingletonFactory(signer, chainId, deployment.address);
|
||||
}
|
||||
|
||||
static async from(signerOrFactory: ethers.Signer | SafeSingletonFactory) {
|
||||
if (signerOrFactory instanceof SafeSingletonFactory) {
|
||||
return signerOrFactory;
|
||||
}
|
||||
|
||||
return await SafeSingletonFactory.init(signerOrFactory);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
) {
|
||||
return this.viewer.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): Promise<boolean> {
|
||||
return this.viewer.isDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): Promise<ReturnType<CFC['connect']> | undefined> {
|
||||
const contract = await this.viewer.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrDeploy<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): Promise<ReturnType<CFC['connect']>> {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode !== '0x') {
|
||||
return ContractFactoryConstructor.connect(
|
||||
address,
|
||||
this.signer,
|
||||
) as ReturnType<CFC['connect']>;
|
||||
}
|
||||
|
||||
const deployTx = {
|
||||
to: this.address,
|
||||
data: ethers.solidityPacked(['uint256', 'bytes'], [salt, initCode]),
|
||||
};
|
||||
|
||||
let receipt;
|
||||
let failedAttempts = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
receipt = await (await this.signer.sendTransaction(deployTx)).wait();
|
||||
break;
|
||||
} catch (error) {
|
||||
const errorCode = (error as { code: unknown }).code;
|
||||
|
||||
if (errorCode === 'NONCE_EXPIRED') {
|
||||
failedAttempts += 1;
|
||||
|
||||
if (failedAttempts >= 3) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (errorCode === 'INSUFFICIENT_FUNDS') {
|
||||
const gasEstimate = await this.provider.estimateGas(deployTx);
|
||||
const { gasPrice } = await this.provider.getFeeData();
|
||||
|
||||
if (!gasPrice) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const balance = await this.provider.getBalance(
|
||||
this.signer.getAddress(),
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
'Account',
|
||||
await this.signer.getAddress(),
|
||||
'has insufficient funds:',
|
||||
ethers.formatEther(balance),
|
||||
'ETH, need (approx):',
|
||||
ethers.formatEther(gasEstimate * gasPrice),
|
||||
'ETH',
|
||||
].join(' '),
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (receipt === null) {
|
||||
throw new Error('Failed to get transaction receipt for deployment');
|
||||
}
|
||||
|
||||
const deployedCode = await this.provider.getCode(
|
||||
address,
|
||||
receipt.blockNumber,
|
||||
);
|
||||
|
||||
assert(deployedCode !== '0x', 'Failed to deploy to expected address');
|
||||
|
||||
return ContractFactoryConstructor.connect(
|
||||
address,
|
||||
this.signer,
|
||||
) as ReturnType<CFC['connect']>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeSingletonFactoryViewer {
|
||||
safeSingletonFactoryAddress: string;
|
||||
signer?: Signer;
|
||||
provider: ethers.Provider;
|
||||
|
||||
constructor(
|
||||
public signerOrProvider: SignerOrProvider,
|
||||
public chainId: bigint,
|
||||
) {
|
||||
this.safeSingletonFactoryAddress =
|
||||
SafeSingletonFactory.deployments[Number(chainId)]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
let provider: ethers.Provider | undefined;
|
||||
|
||||
if ('getNetwork' in signerOrProvider) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
assert(signerOrProvider.provider);
|
||||
provider = signerOrProvider.provider;
|
||||
this.signer = signerOrProvider;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No provider found');
|
||||
}
|
||||
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
static async from(signerOrProvider: SignerOrProvider) {
|
||||
const provider =
|
||||
'getNetwork' in signerOrProvider
|
||||
? signerOrProvider
|
||||
: signerOrProvider.provider;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error('No provider found');
|
||||
}
|
||||
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
return new SafeSingletonFactoryViewer(signerOrProvider, network.chainId);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
) {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
return ethers.getCreate2Address(
|
||||
this.safeSingletonFactoryAddress,
|
||||
salt,
|
||||
ethers.keccak256(initCode),
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
) {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
return existingCode !== '0x';
|
||||
}
|
||||
|
||||
connectAssume<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): ReturnType<CFC['connect']> {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const contract = ContractFactoryConstructor.connect(
|
||||
address,
|
||||
this.signer ?? this.provider,
|
||||
) as ReturnType<CFC['connect']>;
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): Promise<ReturnType<CFC['connect']> | undefined> {
|
||||
const contract = this.connectAssume(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(contract.getAddress());
|
||||
|
||||
if (existingCode === '0x') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrThrow<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.BytesLike = ethers.solidityPacked(['uint256'], [0]),
|
||||
): Promise<ReturnType<CFC['connect']>> {
|
||||
const contract = await this.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(
|
||||
`Contract ${
|
||||
ContractFactoryConstructor.name
|
||||
} not deployed at ${this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
5
tools/fee-measurer/src/SignerOrProvider.ts
Normal file
5
tools/fee-measurer/src/SignerOrProvider.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
type SignerOrProvider = ethers.Signer | ethers.Provider;
|
||||
|
||||
export default SignerOrProvider;
|
||||
14
tools/fee-measurer/src/assert.ts
Normal file
14
tools/fee-measurer/src/assert.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default function assert(
|
||||
condition: unknown,
|
||||
msg = 'Assertion failed',
|
||||
): asserts condition {
|
||||
if (!condition) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger;
|
||||
throw new AssertionError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class AssertionError extends Error {
|
||||
name = 'AssertionError';
|
||||
}
|
||||
Reference in New Issue
Block a user