import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import ganache from 'ganache'; import { BrowserProvider, ContractFactory, parseEther, ZeroAddress, keccak256 } from 'ethers'; const a=JSON.parse(readFileSync('artifacts/contracts.json','utf8')); test('vesting contracts: atomic funding, cliff boundaries, claims, isolation and invalid inputs',async t=>{ const rpc=ganache.provider({logging:{quiet:true},chain:{chainId:46630,hardfork:'shanghai'},miner:{timestampIncrement:0},wallet:{totalAccounts:3}}); t.after(()=>rpc.disconnect()); const p=new BrowserProvider(rpc);p.pollingInterval=10; const owner=await p.getSigner(0),beneficiary=await p.getSigner(1),stranger=await p.getSigner(2); const deploy=async name=>{const c=await new ContractFactory(a[name].abi,a[name].bytecode,owner).deploy();await c.waitForDeployment();return c;}; const v=await deploy('Veylock'),token=await deploy('TestToken'),tax=await deploy('TaxToken'),addr=await v.getAddress(),tokenAddr=await token.getAddress(),recipient=await beneficiary.getAddress(); assert.equal(keccak256(await p.getCode(addr)),keccak256(a.Veylock.runtimeBytecode),'wallet verifies exact workspace code'); const block=await p.getBlock('latest'),start=Number(block.timestamp)+100,cliff=start+100,end=start+400,total=1001n; await (await token.approve(addr,100000n)).wait(); await t.test('creation atomically locks full allocation and indexes creator/beneficiary',async()=>{ await (await v.createSchedule(tokenAddr,recipient,total,start,cliff,end)).wait(); assert.equal(await token.balanceOf(addr),total);assert.equal((await v.schedules(1)).total,total); assert.deepEqual(Array.from(await v.scheduleIds(await owner.getAddress(),0,100)),[1n]); assert.deepEqual(Array.from(await v.scheduleIds(recipient,0,100)),[1n]); assert.deepEqual(Array.from(await v.scheduleIds(recipient,10,100)),[]); }); await t.test('zero addresses, zero amounts, past starts, reversed dates are rejected',async()=>{ for(const args of [[ZeroAddress,recipient,1n,start,cliff,end],[tokenAddr,ZeroAddress,1n,start,cliff,end],[tokenAddr,addr,1n,start,cliff,end],[tokenAddr,recipient,0n,start,cliff,end],[tokenAddr,recipient,1n,start-1000,cliff,end],[tokenAddr,recipient,1n,start,start-1,end],[tokenAddr,recipient,1n,start,end+1,end],[tokenAddr,recipient,1n,start,start,start]])await assert.rejects(v.createSchedule.staticCall(...args)); assert.equal(await v.scheduleCount(),1n); }); await t.test('fee-on-transfer deposit reverts without losing creator tokens',async()=>{ await (await tax.approve(addr,10000n)).wait();const before=await tax.balanceOf(await owner.getAddress()); await assert.rejects(v.createSchedule.staticCall(await tax.getAddress(),recipient,10000n,start,cliff,end)); assert.equal(await tax.balanceOf(await owner.getAddress()),before);assert.equal(await tax.balanceOf(addr),0n); }); await t.test('claim before cliff is impossible and cliff catches up',async()=>{ assert.equal(await v.vestedAmount(1,cliff-1),0n);assert.equal(await v.vestedAmount(1,cliff),250n); assert.equal(await v.vestedAmount(1,start+200),500n);assert.equal(await v.vestedAmount(1,end),total); await assert.rejects(v.claim.staticCall(1));await assert.rejects(v.claim.staticCall(999)); }); await t.test('independent schedules remain funded and self-beneficiary is indexed once',async()=>{ await (await v.createSchedule(tokenAddr,await owner.getAddress(),2000n,start,cliff,end+100)).wait(); assert.deepEqual(Array.from(await v.scheduleIds(await owner.getAddress(),0,100)),[1n,2n]); assert.equal(await token.balanceOf(addr),3001n); }); await t.test('a third party can trigger payment only to the beneficiary',async()=>{ await rpc.request({method:'evm_setTime',params:[(start+200)*1000]});await rpc.request({method:'evm_mine',params:[]}); await (await v.connect(stranger).claim(1)).wait(); const s=await v.schedules(1);assert.equal(s.released,500n);assert.equal(await token.balanceOf(recipient),500n);assert.equal(await token.balanceOf(await stranger.getAddress()),0n); await assert.rejects(v.claim.staticCall(1)); }); await t.test('final claim releases rounding dust and never drains another schedule',async()=>{ await rpc.request({method:'evm_setTime',params:[(end+10)*1000]});await rpc.request({method:'evm_mine',params:[]}); await (await v.claim(1)).wait();assert.equal(await token.balanceOf(recipient),total);assert.equal((await v.schedules(1)).released,total); assert.equal(await token.balanceOf(addr),2000n);await assert.rejects(v.claim.staticCall(1)); }); await t.test('unsolicited deposits do not increase a schedule allocation',async()=>{ await (await token.transfer(addr,parseEther('1'))).wait();assert.equal((await v.schedules(1)).total,total);assert.equal(await v.vestedAmount(1,end+10000),total); }); }); test('sender-side transfer fees cannot consume another allocation during a claim',async t=>{ const rpc=ganache.provider({logging:{quiet:true},chain:{chainId:46630,hardfork:'shanghai'},miner:{timestampIncrement:0},wallet:{totalAccounts:2}}); t.after(()=>rpc.disconnect()); const p=new BrowserProvider(rpc);p.pollingInterval=10; const owner=await p.getSigner(0),recipient=await (await p.getSigner(1)).getAddress(); const deploy=async name=>{const c=await new ContractFactory(a[name].abi,a[name].bytecode,owner).deploy();await c.waitForDeployment();return c;}; const v=await deploy('Veylock'),token=await deploy('SenderTaxToken'),addr=await v.getAddress(),tokenAddr=await token.getAddress(); const start=Number((await p.getBlock('latest')).timestamp)+100,end=start+100; await (await token.approve(addr,20000n)).wait(); const before=await token.balanceOf(await owner.getAddress()); await assert.rejects(async()=>{await (await v.createSchedule(tokenAddr,recipient,10000n,start,start,end,{gasLimit:500000})).wait();}); assert.equal(await token.balanceOf(await owner.getAddress()),before); assert.equal(await token.balanceOf(addr),0n); assert.equal(await v.scheduleCount(),0n); await (await token.setTaxEnabled(false)).wait(); for(let i=0;i<2;i++)await (await v.createSchedule(tokenAddr,recipient,10000n,start,start,end)).wait(); // An issuer can introduce fees after funding: neither schedule may subsidize them. await (await token.setTaxEnabled(true)).wait(); await rpc.request({method:'evm_setTime',params:[(end+1)*1000]});await rpc.request({method:'evm_mine',params:[]}); // Send a mined transaction, not just eth_call: all balance and released writes must roll back. await assert.rejects(async()=>{await (await v.claim(1,{gasLimit:500000})).wait();}); assert.equal(await token.balanceOf(addr),20000n); assert.equal(await token.balanceOf(recipient),0n); assert.equal((await v.schedules(1)).released,0n); assert.equal((await v.schedules(2)).released,0n); });