363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
import process from 'process';
|
|
import path from 'path';
|
|
import { readFile, stat } from 'fs/promises';
|
|
import { ethers } from 'hardhat';
|
|
import { ContractTransactionReceipt, Log, parseUnits, Signer, TransactionReceipt } from 'ethers';
|
|
import type { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers';
|
|
import type { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider';
|
|
import {
|
|
ETHTornado__factory,
|
|
Echoer__factory,
|
|
SingletonFactory__factory,
|
|
SingletonFactory,
|
|
Deployer__factory,
|
|
Deployer,
|
|
Verifier__factory,
|
|
TornadoProxyLight__factory,
|
|
Verifier,
|
|
TornadoProxyLight,
|
|
Echoer,
|
|
ApprovalTornado__factory,
|
|
ETHTornado,
|
|
ApprovalTornado,
|
|
ERC20Mock__factory,
|
|
} from '../typechain-types';
|
|
import { ERC20__factory } from '../typechain-types/factories/@openzeppelin/contracts/token/ERC20';
|
|
|
|
const { parseEther, getSigners } = ethers;
|
|
|
|
const singletonFundingTx = {
|
|
to: '0xBb6e024b9cFFACB947A71991E386681B1Cd1477D',
|
|
value: parseEther('0.0247'),
|
|
};
|
|
|
|
const DEPLOY_DAI = process.env.DEPLOY_DAI === 'true';
|
|
|
|
const salt = '0x0000000000000000000000000000000000000000000000000000000047941987';
|
|
|
|
const merkleHeight = 20;
|
|
|
|
/**
|
|
* Expected deterministic addresses
|
|
*
|
|
* The value here will be only used for checking if the pre deployed contracts exists,
|
|
* so make sure you double-check the deployed result
|
|
*/
|
|
const addresses = {
|
|
singleton: '0xce0042B868300000d44A59004Da54A005ffdcf9f',
|
|
deployer: '0xd408455e186CD6447A196DdA6BBaFa6556a2555F',
|
|
hasher: '0xBaffBE0e6C73d4dAd3f813194695FDC5829c962A',
|
|
verifier: '0xfc9859303c0ac1a7721ECE639f2e249D8FD72ac6',
|
|
proxyLight: '0x0D5550d52428E7e3175bfc9550207e4ad3859b17',
|
|
echoer: '0xa75BF2815618872f155b7C4B0C81bF990f5245E4',
|
|
};
|
|
|
|
interface Pools {
|
|
amount: string;
|
|
symbol: string;
|
|
tokenAddress?: string;
|
|
}
|
|
|
|
async function existsAsync(fileOrDir: string): Promise<boolean> {
|
|
try {
|
|
await stat(fileOrDir);
|
|
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function getPools(): Promise<Pools[]> {
|
|
if (await existsAsync('./pools.json')) {
|
|
console.log('Reading pool config from ./pools.json');
|
|
return JSON.parse(await readFile('./pools.json', { encoding: 'utf8' })) as Pools[];
|
|
}
|
|
|
|
console.log('Using default pool only do this on testing environment or L2s using ETH');
|
|
return JSON.parse(await readFile(path.join(__dirname, './defaultPools.json'), { encoding: 'utf8' })) as Pools[];
|
|
}
|
|
|
|
// Only for testing DAI do not use this on real chain and deploy or connect token for yourself
|
|
async function getDaiPools(owner: SignerWithAddress): Promise<Pools[]> {
|
|
if (!DEPLOY_DAI) {
|
|
return [];
|
|
}
|
|
|
|
const DAIM = await new ERC20Mock__factory(owner).deploy();
|
|
|
|
const pools = JSON.parse(
|
|
await readFile(path.join(__dirname, './defaultTokenPools.json'), { encoding: 'utf8' }),
|
|
) as Pools[];
|
|
|
|
pools.forEach((p) => {
|
|
p.symbol = 'DAI';
|
|
p.tokenAddress = DAIM.target as string;
|
|
});
|
|
|
|
return pools;
|
|
}
|
|
|
|
// Deploy ERC-2470: Singleton Factory
|
|
async function deploySingletonFactory(owner: SignerWithAddress): Promise<SingletonFactory> {
|
|
// Return existing contract if it has
|
|
if ((await owner.provider.getCode(addresses.singleton)) !== '0x') {
|
|
return SingletonFactory__factory.connect(addresses.singleton, owner);
|
|
}
|
|
|
|
console.log('Funding SingletonFactory deployer with 0.0247 ETH');
|
|
|
|
await (await owner.sendTransaction(singletonFundingTx)).wait();
|
|
|
|
console.log('Sending SingletonFactory deployment tx');
|
|
|
|
const { contractAddress } = (await (
|
|
await owner.provider.broadcastTransaction(
|
|
await readFile(path.join(__dirname, './bytecodes/singletonFactoryTx.txt'), { encoding: 'utf8' }),
|
|
)
|
|
).wait()) as TransactionReceipt;
|
|
|
|
return SingletonFactory__factory.connect(contractAddress as string, owner);
|
|
}
|
|
|
|
// Deploy Tornado Deployer
|
|
async function deployDeployer(singleton: SingletonFactory): Promise<Deployer> {
|
|
// Return existing contract if it has
|
|
if ((await (singleton.runner?.provider as HardhatEthersProvider).getCode(addresses.deployer)) !== '0x') {
|
|
return Deployer__factory.connect(addresses.deployer, singleton.runner);
|
|
}
|
|
|
|
console.log('Deploying Tornado Deployer');
|
|
|
|
const receipt = (await (
|
|
await singleton.deploy(
|
|
await readFile(path.join(__dirname, './bytecodes/deployer.txt'), { encoding: 'utf8' }),
|
|
salt,
|
|
)
|
|
).wait()) as ContractTransactionReceipt;
|
|
|
|
const deployerInterface = Deployer__factory.createInterface();
|
|
|
|
const { topicHash } = deployerInterface.getEvent('Deployed');
|
|
|
|
const deployLog = deployerInterface.parseLog(receipt.logs.find((log) => log.topics[0] === topicHash) as Log);
|
|
|
|
return Deployer__factory.connect(deployLog?.args.addr as string, singleton.runner as Signer);
|
|
}
|
|
|
|
async function deployDeterministic(deployer: Deployer, bytecodes: string): Promise<string> {
|
|
const receipt = (await (await deployer.deploy(bytecodes, salt)).wait()) as ContractTransactionReceipt;
|
|
|
|
const { topicHash } = deployer.interface.getEvent('Deployed');
|
|
|
|
const deployLog = deployer.interface.parseLog(receipt.logs.find((log) => log.topics[0] === topicHash) as Log);
|
|
|
|
return deployLog?.args.addr as string;
|
|
}
|
|
|
|
async function deployHasher(deployer: Deployer): Promise<string> {
|
|
// Return existing contract if it has
|
|
if ((await (deployer.runner?.provider as HardhatEthersProvider).getCode(addresses.hasher)) !== '0x') {
|
|
return addresses.hasher;
|
|
}
|
|
|
|
console.log('Deploying Tornado Hasher');
|
|
|
|
return await deployDeterministic(
|
|
deployer,
|
|
await readFile(path.join(__dirname, './bytecodes/hasher.txt'), { encoding: 'utf8' }),
|
|
);
|
|
}
|
|
|
|
async function deployVerifier(deployer: Deployer): Promise<Verifier> {
|
|
// Return existing contract if it has
|
|
if ((await (deployer.runner?.provider as HardhatEthersProvider).getCode(addresses.verifier)) !== '0x') {
|
|
return Verifier__factory.connect(addresses.verifier, deployer.runner);
|
|
}
|
|
|
|
console.log('Deploying Tornado Verifier');
|
|
|
|
return Verifier__factory.connect(
|
|
await deployDeterministic(
|
|
deployer,
|
|
await readFile(path.join(__dirname, './bytecodes/verifier.txt'), { encoding: 'utf8' }),
|
|
),
|
|
deployer.runner,
|
|
);
|
|
}
|
|
|
|
async function deployProxyLight(deployer: Deployer): Promise<TornadoProxyLight> {
|
|
// Return existing contract if it has
|
|
if ((await (deployer.runner?.provider as HardhatEthersProvider).getCode(addresses.proxyLight)) !== '0x') {
|
|
return TornadoProxyLight__factory.connect(addresses.proxyLight, deployer.runner);
|
|
}
|
|
|
|
console.log('Deploying Tornado Proxy Light');
|
|
|
|
return TornadoProxyLight__factory.connect(
|
|
await deployDeterministic(
|
|
deployer,
|
|
await readFile(path.join(__dirname, './bytecodes/proxyLight.txt'), { encoding: 'utf8' }),
|
|
),
|
|
deployer.runner,
|
|
);
|
|
}
|
|
|
|
async function deployEchoer(deployer: Deployer): Promise<Echoer> {
|
|
// Return existing contract if it has
|
|
if ((await (deployer.runner?.provider as HardhatEthersProvider).getCode(addresses.echoer)) !== '0x') {
|
|
return Echoer__factory.connect(addresses.echoer, deployer.runner);
|
|
}
|
|
|
|
console.log('Deploying Tornado Echoer');
|
|
|
|
return Echoer__factory.connect(
|
|
await deployDeterministic(
|
|
deployer,
|
|
await readFile(path.join(__dirname, './bytecodes/echoer.txt'), { encoding: 'utf8' }),
|
|
),
|
|
deployer.runner,
|
|
);
|
|
}
|
|
|
|
async function deployEthInstance(
|
|
deployer: Deployer,
|
|
verifer: Verifier,
|
|
hasher: string,
|
|
denomination: bigint,
|
|
): Promise<ETHTornado> {
|
|
// Get encoded data including contract constructors
|
|
// Should deploy contract code identical to https://bscscan.com/address/0x84443CFd09A48AF6eF360C6976C5392aC5023a1F#code
|
|
// and identical address if denomination is same
|
|
const { data } = await new ETHTornado__factory(
|
|
ETHTornado__factory.abi,
|
|
await readFile(path.join(__dirname, './bytecodes/ethInstance.txt'), { encoding: 'utf8' }),
|
|
deployer.runner as Signer,
|
|
).getDeployTransaction(verifer.target, hasher, denomination, merkleHeight);
|
|
|
|
console.log(`Deploying ${denomination} ETH Tornado`);
|
|
|
|
return ETHTornado__factory.connect(await deployDeterministic(deployer, data), deployer.runner);
|
|
}
|
|
|
|
async function deployErcInstance(
|
|
deployer: Deployer,
|
|
verifer: Verifier,
|
|
hasher: string,
|
|
denomination: bigint,
|
|
token: string,
|
|
proxyLight: TornadoProxyLight,
|
|
): Promise<ApprovalTornado> {
|
|
// Get encoded data including contract constructors
|
|
const { data } = await new ApprovalTornado__factory(
|
|
ApprovalTornado__factory.abi,
|
|
await readFile(path.join(__dirname, './bytecodes/ercInstance.txt'), { encoding: 'utf8' }),
|
|
deployer.runner as Signer,
|
|
).getDeployTransaction(verifer.target, hasher, denomination, merkleHeight, token, proxyLight.target);
|
|
|
|
console.log(`Deploying ${denomination} ${token} ERC Tornado`);
|
|
|
|
return ApprovalTornado__factory.connect(await deployDeterministic(deployer, data), deployer.runner);
|
|
}
|
|
|
|
async function deploy() {
|
|
const [owner] = await getSigners();
|
|
|
|
console.log(`Connected with ${(await owner.provider.getNetwork()).chainId}`);
|
|
|
|
const singleton = await deploySingletonFactory(owner);
|
|
|
|
const deployer = await deployDeployer(singleton);
|
|
|
|
const hasher = await deployHasher(deployer);
|
|
|
|
const verifier = await deployVerifier(deployer);
|
|
|
|
const proxyLight = await deployProxyLight(deployer);
|
|
|
|
const echoer = await deployEchoer(deployer);
|
|
|
|
const pools = [...(await getPools()), ...(await getDaiPools(owner))];
|
|
|
|
const instances = [] as {
|
|
instanceAddress: string;
|
|
amount: string;
|
|
tokenAddress?: string;
|
|
symbol: string;
|
|
decimals: number;
|
|
}[];
|
|
|
|
for (const pool of pools) {
|
|
if (!pool.tokenAddress) {
|
|
const { target } = await deployEthInstance(deployer, verifier, hasher, parseEther(pool.amount));
|
|
|
|
instances.push({
|
|
instanceAddress: target as string,
|
|
amount: pool.amount,
|
|
symbol: pool.symbol,
|
|
decimals: 18,
|
|
});
|
|
} else {
|
|
const token = ERC20__factory.connect(pool.tokenAddress, owner);
|
|
|
|
const [decimals, symbol] = await Promise.all([token.decimals(), token.symbol()]);
|
|
|
|
const { target } = await deployErcInstance(
|
|
deployer,
|
|
verifier,
|
|
hasher,
|
|
parseUnits(pool.amount, Number(decimals)),
|
|
pool.tokenAddress,
|
|
proxyLight,
|
|
);
|
|
|
|
instances.push({
|
|
instanceAddress: target as string,
|
|
amount: pool.amount,
|
|
tokenAddress: pool.tokenAddress,
|
|
symbol: pool.symbol || symbol,
|
|
decimals: Number(decimals),
|
|
});
|
|
}
|
|
}
|
|
|
|
const deployedBlock = await owner.provider.getBlockNumber();
|
|
|
|
const config = {
|
|
deployedBlock,
|
|
routerContract: proxyLight.target,
|
|
echoContract: echoer.target,
|
|
tokens: instances.reduce(
|
|
(acc, curr) => {
|
|
if (!acc[curr.symbol.toLowerCase()]) {
|
|
acc[curr.symbol.toLowerCase()] = {
|
|
instanceAddress: {},
|
|
tokenAddress: curr.tokenAddress,
|
|
symbol: curr.symbol,
|
|
decimals: curr.decimals,
|
|
};
|
|
}
|
|
|
|
if (!acc[curr.symbol.toLowerCase()].instanceAddress[curr.amount]) {
|
|
acc[curr.symbol.toLowerCase()].instanceAddress[curr.amount] = curr.instanceAddress;
|
|
}
|
|
|
|
return acc;
|
|
},
|
|
{} as {
|
|
[key: string]: {
|
|
instanceAddress: {
|
|
[key: string]: string;
|
|
};
|
|
tokenAddress?: string;
|
|
symbol: string;
|
|
decimals: number;
|
|
};
|
|
},
|
|
),
|
|
};
|
|
|
|
console.log(JSON.stringify(config, null, 2));
|
|
}
|
|
deploy();
|