-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Feature/vesting wallet factory #6524
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a90a55c
a1aa44e
d9e223b
22059ab
99218cf
b03a7e3
2c1fceb
19e06ab
fb0e7c6
6f62fc8
cc8c8f8
cb012d6
f76fd41
5d5cf24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| // OpenZeppelin Contracts (last updated v5.6.0) (finance/VestingWallet.sol) | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IERC20} from "../token/ERC20/IERC20.sol"; | ||
| import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol"; | ||
| import {Ownable} from "../access/Ownable.sol"; | ||
|
|
||
| contract VestingWalletFactory is Ownable { | ||
| struct VestingSchedule { | ||
| address beneficiary; | ||
| address token; | ||
| uint64 start; | ||
| uint64 duration; | ||
| uint256 totalAllocation; | ||
| uint256 released; | ||
| } | ||
|
|
||
| mapping(uint256 scheduleId => VestingSchedule) private _schedules; | ||
| uint256 private _scheduleCount; | ||
|
|
||
| constructor(address owner) Ownable(owner) {} | ||
|
|
||
| event VestingScheduleCreated(uint256 indexed scheduleId, address indexed beneficiary, address indexed token, uint64 start, uint64 duration, uint256 amount); | ||
| event ERC20Released(uint256 indexed scheduleId, address indexed token, uint256 amount); | ||
|
|
||
| function createVestingSchedule( | ||
| address beneficiary, | ||
| address token, | ||
| uint64 startTimestamp, | ||
| uint64 durationSeconds, | ||
| uint256 amount | ||
| ) external onlyOwner returns (uint256 scheduleId) { | ||
| require(beneficiary != address(0), "VestingWalletFactory: beneficiary is zero address"); | ||
| require(amount > 0, "VestingWalletFactory: amount is zero"); | ||
| require(durationSeconds > 0, "VestingWalletFactory: duration is zero"); | ||
|
|
||
| scheduleId = _scheduleCount; | ||
|
|
||
| _schedules[scheduleId] = VestingSchedule({ | ||
| beneficiary: beneficiary, | ||
| token: token, | ||
| start: startTimestamp, | ||
| duration: durationSeconds, | ||
| totalAllocation: amount, | ||
| released: 0 | ||
| }); | ||
|
|
||
| _scheduleCount++; | ||
|
|
||
| SafeERC20.safeTransferFrom(IERC20(token), msg.sender, address(this), amount); | ||
|
|
||
| emit VestingScheduleCreated(scheduleId, beneficiary, token, startTimestamp, durationSeconds, amount); | ||
| } | ||
|
|
||
| function vestedAmount(uint256 scheduleId, uint64 timestamp) public view returns (uint256) { | ||
| VestingSchedule storage schedule = _schedules[scheduleId]; | ||
| return _vestingSchedule(schedule.totalAllocation, schedule.start, schedule.duration, timestamp); | ||
| } | ||
|
|
||
| function releasable(uint256 scheduleId) public view returns (uint256) { | ||
| VestingSchedule storage schedule = _schedules[scheduleId]; | ||
| return vestedAmount(scheduleId, uint64(block.timestamp)) - schedule.released; | ||
| } | ||
|
|
||
| function getSchedule(uint256 scheduleId) external view returns (VestingSchedule memory) { | ||
| return _schedules[scheduleId]; | ||
| } | ||
|
|
||
| function scheduleCount() external view returns (uint256) { | ||
| return _scheduleCount; | ||
| } | ||
|
|
||
| function release(uint256 scheduleId) external { | ||
| VestingSchedule storage schedule = _schedules[scheduleId]; | ||
| uint256 amount = releasable(scheduleId); | ||
| schedule.released += amount; | ||
| SafeERC20.safeTransfer(IERC20(schedule.token), schedule.beneficiary, amount); | ||
| emit ERC20Released(scheduleId, schedule.token, amount); | ||
| } | ||
|
|
||
| function _vestingSchedule(uint256 totalAllocation, uint64 start, uint64 duration, uint64 timestamp) internal pure virtual returns (uint256) { | ||
| uint64 end = start + duration; | ||
| if (timestamp < start) { | ||
| return 0; | ||
| } | ||
| else if (timestamp >= end) { | ||
| return totalAllocation; | ||
| } | ||
| else { | ||
| return (totalAllocation * (timestamp - start)) / duration; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| // Local demo: VestingWalletFactory end-to-end walkthrough. | ||
| // | ||
| // Usage: | ||
| // npx hardhat run scripts/demoFactory.js | ||
|
|
||
| const { ethers } = require('hardhat'); | ||
|
|
||
| const DECIMALS = 18n; | ||
| const UNIT = 10n ** DECIMALS; | ||
| const fmt = n => (Number(n) / Number(UNIT)).toFixed(2); | ||
| const fmtAddr = a => a.slice(0, 6) + '...' + a.slice(-4); | ||
|
|
||
| async function printSchedule(factory, token, id, label) { | ||
| const s = await factory.getSchedule(id); | ||
| const releasable = await factory.releasable(id); | ||
| const balance = await token.balanceOf(s.beneficiary); | ||
| console.log(` [Schedule ${id}] ${label}`); | ||
| console.log(` beneficiary : ${fmtAddr(s.beneficiary)}`); | ||
| console.log(` allocation : ${fmt(s.totalAllocation)} tokens`); | ||
| console.log(` released : ${fmt(s.released)} tokens`); | ||
| console.log(` releasable : ${fmt(releasable)} tokens`); | ||
| console.log(` wallet bal : ${fmt(balance)} tokens`); | ||
| } | ||
|
|
||
| async function main() { | ||
| const signers = await ethers.getSigners(); | ||
| const [owner, alice, bob, carol] = signers; | ||
|
|
||
| const block = await ethers.provider.getBlock('latest'); | ||
| const now = BigInt(block.timestamp); | ||
|
|
||
| // ── 1. Deploy token & factory ───────────────────────────────────────────── | ||
|
|
||
| console.log('\n══════════════════════════════════════════════════'); | ||
| console.log(' VestingWalletFactory — Local Demo'); | ||
| console.log('══════════════════════════════════════════════════\n'); | ||
|
|
||
| const token = await ethers.deployContract('$ERC20', ['Demo Token', 'DMT']); | ||
| const factory = await ethers.deployContract('VestingWalletFactory', [owner.address]); | ||
|
|
||
| console.log('Contracts deployed'); | ||
| console.log(` Token : ${token.target}`); | ||
| console.log(` Factory : ${factory.target}`); | ||
|
|
||
| // ── 2. Mint tokens to owner and approve factory ─────────────────────────── | ||
|
|
||
| const totalSupply = 3000n * UNIT; | ||
| await token.$_mint(owner.address, totalSupply); | ||
| await token.connect(owner).approve(factory.target, ethers.MaxUint256); | ||
|
|
||
| console.log(`\nMinted ${fmt(totalSupply)} DMT to owner, factory approved.\n`); | ||
|
|
||
| // ── 3. Create three vesting schedules ───────────────────────────────────── | ||
|
|
||
| const start = now + 60n; // starts in 1 minute | ||
| const oneYear = BigInt(365 * 24 * 3600); | ||
| const sixMonth = oneYear / 2n; | ||
|
|
||
| // Alice: 1000 tokens over 1 year | ||
| await factory.createVestingSchedule(alice.address, token.target, start, oneYear, 1000n * UNIT); | ||
| // Bob: 500 tokens over 6 months | ||
| await factory.createVestingSchedule(bob.address, token.target, start, sixMonth, 500n * UNIT); | ||
| // Carol: 1500 tokens over 1 year | ||
| await factory.createVestingSchedule(carol.address, token.target, start, oneYear, 1500n * UNIT); | ||
|
|
||
| console.log('Vesting schedules created:'); | ||
| console.log(` Schedule 0 — Alice : 1000 DMT over 12 months`); | ||
| console.log(` Schedule 1 — Bob : 500 DMT over 6 months`); | ||
| console.log(` Schedule 2 — Carol : 1500 DMT over 12 months`); | ||
|
|
||
| // ── 4. State at t=0 ─────────────────────────────────────────────────────── | ||
|
|
||
| console.log('\n──────────────────────────────────────────────────'); | ||
| console.log(' State at T=0 (vesting just started)'); | ||
| console.log('──────────────────────────────────────────────────'); | ||
| await printSchedule(factory, token, 0n, 'Alice'); | ||
| await printSchedule(factory, token, 1n, 'Bob'); | ||
| await printSchedule(factory, token, 2n, 'Carol'); | ||
|
|
||
| // ── 5. Advance to 6 months ──────────────────────────────────────────────── | ||
|
|
||
| await ethers.provider.send('evm_increaseTime', [Number(sixMonth) + 60]); | ||
| await ethers.provider.send('evm_mine'); | ||
|
|
||
| console.log('\n──────────────────────────────────────────────────'); | ||
| console.log(' State at T=6 months (Bob fully vested)'); | ||
| console.log('──────────────────────────────────────────────────'); | ||
| await printSchedule(factory, token, 0n, 'Alice'); | ||
| await printSchedule(factory, token, 1n, 'Bob'); | ||
| await printSchedule(factory, token, 2n, 'Carol'); | ||
|
|
||
| // ── 6. Release Bob's tokens ─────────────────────────────────────────────── | ||
|
|
||
| console.log('\nReleasing Bob\'s tokens...'); | ||
| await factory.release(1n); | ||
| console.log(' Done.\n'); | ||
| await printSchedule(factory, token, 1n, 'Bob (after release)'); | ||
|
|
||
| // ── 7. Advance to 12 months ─────────────────────────────────────────────── | ||
|
|
||
| await ethers.provider.send('evm_increaseTime', [Number(sixMonth)]); | ||
| await ethers.provider.send('evm_mine'); | ||
|
|
||
| console.log('\n──────────────────────────────────────────────────'); | ||
| console.log(' State at T=12 months (all fully vested)'); | ||
| console.log('──────────────────────────────────────────────────'); | ||
| await printSchedule(factory, token, 0n, 'Alice'); | ||
| await printSchedule(factory, token, 1n, 'Bob'); | ||
| await printSchedule(factory, token, 2n, 'Carol'); | ||
|
|
||
| // ── 8. Release Alice and Carol ──────────────────────────────────────────── | ||
|
|
||
| console.log('\nReleasing Alice and Carol\'s tokens...'); | ||
| await factory.release(0n); | ||
| await factory.release(2n); | ||
| console.log(' Done.\n'); | ||
|
|
||
| // ── 9. Final state ──────────────────────────────────────────────────────── | ||
|
|
||
| console.log('──────────────────────────────────────────────────'); | ||
| console.log(' Final State'); | ||
| console.log('──────────────────────────────────────────────────'); | ||
| await printSchedule(factory, token, 0n, 'Alice'); | ||
| await printSchedule(factory, token, 1n, 'Bob'); | ||
| await printSchedule(factory, token, 2n, 'Carol'); | ||
|
|
||
| const factoryBalance = await token.balanceOf(factory.target); | ||
| console.log(`\n Factory remaining balance: ${fmt(factoryBalance)} DMT`); | ||
| console.log('\n══════════════════════════════════════════════════\n'); | ||
| } | ||
|
|
||
| main().catch(err => { console.error(err); process.exit(1); }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,165 @@ | ||||||||||||||||||||||||||||||||||
| // Gas comparison: N separate VestingWallet contracts vs one VestingWalletFactory. | ||||||||||||||||||||||||||||||||||
| // | ||||||||||||||||||||||||||||||||||
| // Usage: | ||||||||||||||||||||||||||||||||||
| // npx hardhat run scripts/gasCompare.js | ||||||||||||||||||||||||||||||||||
| // N=10 npx hardhat run scripts/gasCompare.js (Linux/macOS) | ||||||||||||||||||||||||||||||||||
| // $env:N="10"; npx hardhat run scripts/gasCompare.js (PowerShell) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const { ethers } = require('hardhat'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const N = parseInt(process.env.N ?? '5'); | ||||||||||||||||||||||||||||||||||
| const AMOUNT = ethers.parseEther('1000'); | ||||||||||||||||||||||||||||||||||
| const DURATION = BigInt(365 * 24 * 3600); // 1 year in seconds | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async function main() { | ||||||||||||||||||||||||||||||||||
| if (N < 1) throw new Error('N must be at least 1'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Line 10/Line 15 currently allow Suggested fix-const N = parseInt(process.env.N ?? '5');
+const rawN = process.env.N ?? '5';
+const N = Number(rawN);
const AMOUNT = ethers.parseEther('1000');
const DURATION = BigInt(365 * 24 * 3600); // 1 year in seconds
async function main() {
- if (N < 1) throw new Error('N must be at least 1');
+ if (!Number.isInteger(N) || N < 1) {
+ throw new Error(`N must be a positive integer; received "${rawN}"`);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| const signers = await ethers.getSigners(); | ||||||||||||||||||||||||||||||||||
| const [owner, ...rest] = signers; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (rest.length < N) { | ||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||
| `N=${N} requires ${N + 1} signers but Hardhat provides ${signers.length}. ` + | ||||||||||||||||||||||||||||||||||
| `Lower N or add more accounts in hardhat.config.js.`, | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const beneficiaries = rest.slice(0, N); | ||||||||||||||||||||||||||||||||||
| const block = await ethers.provider.getBlock('latest'); | ||||||||||||||||||||||||||||||||||
| const start = BigInt(block.timestamp) + 3600n; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const token = await ethers.deployContract('$ERC20', ['Gas Token', 'GT']); | ||||||||||||||||||||||||||||||||||
| console.log(`\nBenchmarking N=${N} beneficiar${N === 1 ? 'y' : 'ies'}...`); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Approach A: one VestingWallet per beneficiary ───────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const walletDeployGas = []; | ||||||||||||||||||||||||||||||||||
| const walletFundGas = []; | ||||||||||||||||||||||||||||||||||
| const walletReleaseGas = []; | ||||||||||||||||||||||||||||||||||
| const wallets = []; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||||||||||||
| await token.$_mint(owner.address, AMOUNT); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const wallet = await ethers.deployContract('VestingWallet', [ | ||||||||||||||||||||||||||||||||||
| beneficiaries[i].address, | ||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||
| DURATION, | ||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||
| walletDeployGas.push((await wallet.deploymentTransaction().wait()).gasUsed); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const fundReceipt = await (await token.connect(owner).transfer(wallet.target, AMOUNT)).wait(); | ||||||||||||||||||||||||||||||||||
| walletFundGas.push(fundReceipt.gasUsed); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| wallets.push(wallet); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Approach B: VestingWalletFactory ───────────────────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const factory = await ethers.deployContract('VestingWalletFactory', [owner.address]); | ||||||||||||||||||||||||||||||||||
| const factoryDeployGas = (await factory.deploymentTransaction().wait()).gasUsed; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| await token.$_mint(owner.address, AMOUNT * BigInt(N)); | ||||||||||||||||||||||||||||||||||
| const approveGas = ( | ||||||||||||||||||||||||||||||||||
| await (await token.connect(owner).approve(factory.target, ethers.MaxUint256)).wait() | ||||||||||||||||||||||||||||||||||
| ).gasUsed; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const factoryCreateGas = []; | ||||||||||||||||||||||||||||||||||
| const factoryReleaseGas = []; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||||||||||||
| const receipt = await ( | ||||||||||||||||||||||||||||||||||
| await factory.createVestingSchedule( | ||||||||||||||||||||||||||||||||||
| beneficiaries[i].address, | ||||||||||||||||||||||||||||||||||
| token.target, | ||||||||||||||||||||||||||||||||||
| start, | ||||||||||||||||||||||||||||||||||
| DURATION, | ||||||||||||||||||||||||||||||||||
| AMOUNT, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| ).wait(); | ||||||||||||||||||||||||||||||||||
| factoryCreateGas.push(receipt.gasUsed); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Advance time past vesting end ───────────────────────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| await ethers.provider.send('evm_increaseTime', [Number(DURATION) + 3601]); | ||||||||||||||||||||||||||||||||||
| await ethers.provider.send('evm_mine'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Release ─────────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (const wallet of wallets) { | ||||||||||||||||||||||||||||||||||
| const receipt = await (await wallet['release(address)'](token.target)).wait(); | ||||||||||||||||||||||||||||||||||
| walletReleaseGas.push(receipt.gasUsed); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| for (let i = 0; i < N; i++) { | ||||||||||||||||||||||||||||||||||
| const receipt = await (await factory.release(BigInt(i))).wait(); | ||||||||||||||||||||||||||||||||||
| factoryReleaseGas.push(receipt.gasUsed); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Aggregate ───────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const sum = arr => arr.reduce((a, b) => a + b, 0n); | ||||||||||||||||||||||||||||||||||
| const avg = arr => sum(arr) / BigInt(arr.length); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const factoryOneTime = factoryDeployGas + approveGas; | ||||||||||||||||||||||||||||||||||
| const walletSetupTotal = sum(walletDeployGas) + sum(walletFundGas); | ||||||||||||||||||||||||||||||||||
| const factorySetupTotal = factoryOneTime + sum(factoryCreateGas); | ||||||||||||||||||||||||||||||||||
| const walletTotal = walletSetupTotal + sum(walletReleaseGas); | ||||||||||||||||||||||||||||||||||
| const factoryTotal = factorySetupTotal + sum(factoryReleaseGas); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ── Print table ─────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const LBL = 38; | ||||||||||||||||||||||||||||||||||
| const W = 14; | ||||||||||||||||||||||||||||||||||
| const SEP = ' ' + '-'.repeat(LBL + W + W + 2); | ||||||||||||||||||||||||||||||||||
| const RULE = ' ' + '='.repeat(LBL + W + W + 2); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Inserts commas and right-pads to W characters | ||||||||||||||||||||||||||||||||||
| const fmt = n => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',').padStart(W); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const row = (label, walletVal, factoryVal) => | ||||||||||||||||||||||||||||||||||
| ` ${label.padEnd(LBL)} ${walletVal} ${factoryVal}`; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||||||
| console.log(RULE); | ||||||||||||||||||||||||||||||||||
| console.log(` Gas Comparison — N = ${N} beneficiar${N === 1 ? 'y' : 'ies'}`); | ||||||||||||||||||||||||||||||||||
| console.log(RULE); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||||||
| console.log(row('SETUP', 'VestingWallet'.padStart(W), 'Factory'.padStart(W))); | ||||||||||||||||||||||||||||||||||
| console.log(SEP); | ||||||||||||||||||||||||||||||||||
| console.log(row(' avg deploy / createSchedule', fmt(avg(walletDeployGas)), fmt(avg(factoryCreateGas)))); | ||||||||||||||||||||||||||||||||||
| console.log(row(' avg fund transfer', fmt(avg(walletFundGas)), '(incl. above)'.padStart(W))); | ||||||||||||||||||||||||||||||||||
| console.log(row(' avg per-beneficiary total', fmt(avg(walletDeployGas) + avg(walletFundGas)), fmt(avg(factoryCreateGas)))); | ||||||||||||||||||||||||||||||||||
| console.log(row(' one-time: deploy + approve', '—'.padStart(W), fmt(factoryOneTime))); | ||||||||||||||||||||||||||||||||||
| console.log(SEP); | ||||||||||||||||||||||||||||||||||
| console.log(row(` total setup (N=${N})`, fmt(walletSetupTotal), fmt(factorySetupTotal))); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||||||
| console.log(row('RELEASE', 'VestingWallet'.padStart(W), 'Factory'.padStart(W))); | ||||||||||||||||||||||||||||||||||
| console.log(SEP); | ||||||||||||||||||||||||||||||||||
| console.log(row(' avg per beneficiary', fmt(avg(walletReleaseGas)), fmt(avg(factoryReleaseGas)))); | ||||||||||||||||||||||||||||||||||
| console.log(SEP); | ||||||||||||||||||||||||||||||||||
| console.log(row(` total release (N=${N})`, fmt(sum(walletReleaseGas)), fmt(sum(factoryReleaseGas)))); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||||||
| console.log(RULE); | ||||||||||||||||||||||||||||||||||
| console.log(row('GRAND TOTAL (setup + release)', fmt(walletTotal), fmt(factoryTotal))); | ||||||||||||||||||||||||||||||||||
| console.log(RULE); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const savings = walletTotal - factoryTotal; | ||||||||||||||||||||||||||||||||||
| if (savings > 0n) { | ||||||||||||||||||||||||||||||||||
| const pct = (Number(savings * 10000n / walletTotal) / 100).toFixed(1); | ||||||||||||||||||||||||||||||||||
| const fmtSavings = savings.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); | ||||||||||||||||||||||||||||||||||
| console.log(`\n Factory saves ${fmtSavings} gas (${pct}% cheaper than separate wallets)`); | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| const extra = -savings; | ||||||||||||||||||||||||||||||||||
| const pct = (Number(extra * 10000n / factoryTotal) / 100).toFixed(1); | ||||||||||||||||||||||||||||||||||
| const fmtExtra = extra.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); | ||||||||||||||||||||||||||||||||||
| console.log(`\n Separate wallets save ${fmtExtra} gas (${pct}% cheaper than factory)`); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| console.log(''); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| main().catch(err => { console.error(err); process.exit(1); }); | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the actual file and surrounding context cat scripts/demoFactory.jsRepository: OpenZeppelin/openzeppelin-contracts
Length of output: 5689
🏁 Script executed:
Repository: OpenZeppelin/openzeppelin-contracts
Length of output: 650
Use
ethers.formatUnits()instead ofNumber(BigInt)for precise token formatting.The values in this demo exceed JavaScript's safe integer limit (2^53 - 1), making direct BigInt-to-Number conversion unsafe even though the output happens to appear correct.
ethers.formatUnits(n, 18)handles this safely and is the idiomatic approach for ethers.js v6.Proposed fix
🤖 Prompt for AI Agents