Skip to content

Commit ae4cc7b

Browse files
Merge pull request #278 from BitGo/WIN-7265
chore: enforced the funds before contract deployment
2 parents c5477ef + 67c50d4 commit ae4cc7b

File tree

3 files changed

+239
-9
lines changed

3 files changed

+239
-9
lines changed

deployUtils.ts

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ import { verifyOnCustomEtherscan } from './scripts/customContractVerifier';
66

77
const OUTPUT_FILE = 'output.json';
88

9+
// Balance check configuration
10+
/**
11+
* Safety multiplier for balance checks.
12+
* A value of 150n with divisor 100n creates a 1.5x safety buffer.
13+
* This ensures we have 50% more funds than the estimated gas cost.
14+
*/
15+
export const BALANCE_SAFETY_MULTIPLIER = 150n; // 1.5x safety buffer (150/100 = 1.5)
16+
export const BALANCE_SAFETY_DIVISOR = 100n;
17+
18+
/**
19+
* Calculates the required amount with safety buffer applied.
20+
* @param estimatedCost - The estimated gas cost in wei
21+
* @returns The required amount with safety buffer applied
22+
*/
23+
export function applyBalanceSafetyBuffer(estimatedCost: bigint): bigint {
24+
return (estimatedCost * BALANCE_SAFETY_MULTIPLIER) / BALANCE_SAFETY_DIVISOR;
25+
}
26+
927
export const logger = {
1028
info: (msg: string) => console.log(`[INFO] ${msg}`),
1129
config: (msg: string) => console.log(`[CONFIG] ${msg}`),
@@ -15,6 +33,86 @@ export const logger = {
1533
step: (msg: string) => console.log(`\n--- ${msg} ---`)
1634
};
1735

36+
/**
37+
* Checks if the deployer has sufficient balance for the deployment.
38+
* Requires 1.5x the estimated gas cost to provide a safety buffer.
39+
*/
40+
export async function checkSufficientBalance(
41+
deployerAddress: string,
42+
estimatedGasCost: bigint,
43+
contractName: string = 'contract'
44+
): Promise<void> {
45+
const balance = await ethers.provider.getBalance(deployerAddress);
46+
const requiredAmount = applyBalanceSafetyBuffer(estimatedGasCost);
47+
48+
logger.info(`💰 Balance check for ${contractName} deployment:`);
49+
logger.info(` Deployer balance: ${ethers.formatEther(balance)} ETH`);
50+
logger.info(
51+
` Estimated gas cost: ${ethers.formatEther(estimatedGasCost)} ETH`
52+
);
53+
logger.info(
54+
` Required (1.5x buffer): ${ethers.formatEther(requiredAmount)} ETH`
55+
);
56+
57+
if (balance < requiredAmount) {
58+
const shortfall = requiredAmount - balance;
59+
logger.error(`Insufficient funds for ${contractName} deployment!`);
60+
logger.error(` Shortfall: ${ethers.formatEther(shortfall)} ETH`);
61+
logger.error(` Please fund the deployer account: ${deployerAddress}`);
62+
throw new Error(
63+
`Insufficient funds: need ${ethers.formatEther(
64+
requiredAmount
65+
)} ETH, have ${ethers.formatEther(balance)} ETH`
66+
);
67+
}
68+
69+
logger.success(
70+
`✅ Sufficient balance confirmed for ${contractName} deployment`
71+
);
72+
}
73+
74+
/**
75+
* Checks balance before individual contract deployment.
76+
* Estimates gas for a typical contract deployment transaction.
77+
*/
78+
export async function checkIndividualContractBalance(
79+
deployerAddress: string,
80+
contractName: string,
81+
gasOverrides?: any
82+
): Promise<void> {
83+
try {
84+
// Get current fee data
85+
const feeData = await ethers.provider.getFeeData();
86+
87+
// Estimate typical contract deployment gas (fallback if we can't estimate precisely)
88+
const estimatedGas = 500000n; // 500k gas - reasonable estimate for contract deployment
89+
90+
// Calculate gas price
91+
let gasPrice: bigint;
92+
if (gasOverrides?.gasPrice) {
93+
gasPrice = gasOverrides.gasPrice;
94+
} else if (gasOverrides?.maxFeePerGas) {
95+
gasPrice = gasOverrides.maxFeePerGas;
96+
} else {
97+
gasPrice = feeData.gasPrice || 1_000_000_000n; // 1 gwei fallback
98+
}
99+
100+
const estimatedCost = estimatedGas * gasPrice;
101+
102+
// Check balance with 1.5x buffer
103+
await checkSufficientBalance(deployerAddress, estimatedCost, contractName);
104+
} catch (error) {
105+
logger.warn(
106+
`⚠️ Could not perform precise balance check for ${contractName}: ${
107+
(error as Error).message
108+
}`
109+
);
110+
logger.info(
111+
`🔄 Proceeding with deployment - will fail fast if insufficient funds`
112+
);
113+
}
114+
}
115+
18116
export type DeploymentAddresses = {
19117
walletImplementation?: string;
20118
walletFactory?: string;
@@ -31,7 +129,8 @@ export async function deployIfNeededAtNonce(
31129
expectedNonce: number,
32130
deployerAddress: string,
33131
contractName: string,
34-
deployFn: () => Promise<string>
132+
deployFn: () => Promise<string>,
133+
gasOverrides?: any
35134
): Promise<string> {
36135
const predictedAddress = getCreateAddress({
37136
from: deployerAddress,
@@ -83,12 +182,19 @@ export async function deployIfNeededAtNonce(
83182
throw new Error(errorMsg);
84183
}
85184

86-
// 5. Deploy
185+
// 5. Balance check before deployment
186+
await checkIndividualContractBalance(
187+
deployerAddress,
188+
contractName,
189+
gasOverrides
190+
);
191+
192+
// 6. Deploy
87193
logger.info(`🚀 Deploying ${contractName} at nonce ${expectedNonce}...`);
88194
return await deployFn();
89195
}
90196

91-
async function isContractDeployed(address: string) {
197+
export async function isContractDeployed(address: string) {
92198
const code = await ethers.provider.getCode(address);
93199
return code && code !== '0x';
94200
}

scripts/deploy.ts

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
waitAndVerify,
66
loadOutput,
77
saveOutput,
8-
DeploymentAddresses
8+
DeploymentAddresses,
9+
checkSufficientBalance,
10+
isContractDeployed
911
} from '../deployUtils';
1012
import { setupBigBlocksForV4Deployment } from './enableBigBlocks';
1113
import { isBigBlocksSupported } from '../config/bigBlocksConfig';
@@ -39,6 +41,99 @@ async function main() {
3941
`🚀 Deployer: ${deployerAddress} (nonce: ${currentNonce}) on chain ${chainId}`
4042
);
4143

44+
// Pre-deployment balance check - estimate total cost for all contracts
45+
console.log('\n--- Checking deployer balance ---');
46+
47+
// Estimate gas costs for all potential deployments
48+
let totalEstimatedCost = 0n;
49+
50+
// Only estimate if we need to deploy (not already deployed)
51+
if (
52+
!output.walletImplementation ||
53+
!(await isContractDeployed(output.walletImplementation))
54+
) {
55+
const WalletSimple = await ethers.getContractFactory(
56+
chainConfig.walletImplementationContractName
57+
);
58+
const walletGas = await deployer.estimateGas({
59+
...(await WalletSimple.getDeployTransaction(...[], gasOverrides)),
60+
from: deployerAddress
61+
});
62+
totalEstimatedCost += walletGas;
63+
}
64+
65+
if (
66+
!output.walletFactory ||
67+
!(await isContractDeployed(output.walletFactory))
68+
) {
69+
const WalletFactory = await ethers.getContractFactory(
70+
chainConfig.walletFactoryContractName
71+
);
72+
const factoryGas = await deployer.estimateGas({
73+
...(await WalletFactory.getDeployTransaction(
74+
deployerAddress,
75+
gasOverrides
76+
)), // Use deployer address as placeholder
77+
from: deployerAddress
78+
});
79+
totalEstimatedCost += factoryGas;
80+
}
81+
82+
if (
83+
!output.forwarderImplementation ||
84+
!(await isContractDeployed(output.forwarderImplementation))
85+
) {
86+
const ForwarderV4 = await ethers.getContractFactory(
87+
chainConfig.forwarderContractName
88+
);
89+
const forwarderGas = await deployer.estimateGas({
90+
...(await ForwarderV4.getDeployTransaction(gasOverrides)),
91+
from: deployerAddress
92+
});
93+
totalEstimatedCost += forwarderGas;
94+
}
95+
96+
if (
97+
!output.forwarderFactory ||
98+
!(await isContractDeployed(output.forwarderFactory))
99+
) {
100+
const ForwarderFactory = await ethers.getContractFactory(
101+
chainConfig.forwarderFactoryContractName
102+
);
103+
const forwarderFactoryGas = await deployer.estimateGas({
104+
...(await ForwarderFactory.getDeployTransaction(
105+
deployerAddress,
106+
gasOverrides
107+
)), // Use deployer address as placeholder
108+
from: deployerAddress
109+
});
110+
totalEstimatedCost += forwarderFactoryGas;
111+
}
112+
113+
if (totalEstimatedCost > 0n) {
114+
// Get gas price for cost calculation
115+
const feeData = await ethers.provider.getFeeData();
116+
let gasPrice: bigint;
117+
118+
if (gasOverrides?.gasPrice) {
119+
gasPrice = gasOverrides.gasPrice;
120+
} else if (gasOverrides?.maxFeePerGas) {
121+
gasPrice = gasOverrides.maxFeePerGas;
122+
} else {
123+
gasPrice = feeData.gasPrice || 1_000_000_000n; // 1 gwei fallback
124+
}
125+
126+
const estimatedCost = totalEstimatedCost * gasPrice;
127+
128+
await checkSufficientBalance(
129+
deployerAddress,
130+
estimatedCost,
131+
'all V4 contracts'
132+
);
133+
} else {
134+
console.log('✅ All contracts already deployed, skipping balance check');
135+
}
136+
42137
// Deploy Wallet Implementation
43138
const walletAddress = await deployIfNeededAtNonce(
44139
output.walletImplementation,
@@ -69,7 +164,8 @@ async function main() {
69164
output.walletImplementation = contract.target as string;
70165
saveOutput(output);
71166
return contract.target as string;
72-
}
167+
},
168+
gasOverrides
73169
);
74170

75171
// Deploy Wallet Factory
@@ -96,7 +192,8 @@ async function main() {
96192
output.walletFactory = contract.target as string;
97193
saveOutput(output);
98194
return contract.target as string;
99-
}
195+
},
196+
gasOverrides
100197
);
101198

102199
// Deploy Forwarder
@@ -118,7 +215,8 @@ async function main() {
118215
output.forwarderImplementation = contract.target as string;
119216
saveOutput(output);
120217
return contract.target as string;
121-
}
218+
},
219+
gasOverrides
122220
);
123221

124222
// Deploy Forwarder Factory
@@ -148,7 +246,8 @@ async function main() {
148246
output.forwarderFactory = contract.target as string;
149247
saveOutput(output);
150248
return contract.target as string;
151-
}
249+
},
250+
gasOverrides
152251
);
153252

154253
console.log(`🎉 All contracts deployed and verified!`);

scripts/deployBatcherContract.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import hre, { ethers } from 'hardhat';
22
import { Contract } from 'ethers';
3-
import { logger, waitAndVerify } from '../deployUtils';
3+
import { logger, waitAndVerify, checkSufficientBalance } from '../deployUtils';
44
import fs from 'fs';
55
import { setupBigBlocksForBatcherDeployment } from './enableBigBlocks';
66
import { isBigBlocksSupported } from '../config/bigBlocksConfig';
@@ -187,6 +187,31 @@ async function main() {
187187
logger.info(`Using default gas parameters for this network.`);
188188
}
189189

190+
// --- 3.5. Balance Check ---
191+
logger.step('3.5. Checking deployer balance before deployment...');
192+
193+
// Estimate gas cost for the deployment
194+
const gasEstimate = await batcherDeployer.estimateGas({
195+
...deployTxReq,
196+
from: address,
197+
...gasOverrides
198+
});
199+
200+
// Calculate total cost
201+
let gasPrice: bigint;
202+
if (gasOverrides?.gasPrice) {
203+
gasPrice = gasOverrides.gasPrice;
204+
} else if (gasOverrides?.maxFeePerGas) {
205+
gasPrice = gasOverrides.maxFeePerGas;
206+
} else {
207+
gasPrice = feeData.gasPrice ?? 1_000_000_000n; // 1 gwei fallback
208+
}
209+
210+
const estimatedCost = gasEstimate * gasPrice;
211+
212+
// Check if we have 1.5x the required amount
213+
await checkSufficientBalance(address, estimatedCost, 'Batcher');
214+
190215
let batcher: Contract;
191216
if (gasOverrides) {
192217
batcher = (await Batcher.deploy(

0 commit comments

Comments
 (0)