const { ethers, network } = require("hardhat"); const contentHash = require("content-hash"); const { expect, assert } = require("chai"); const { time } = require("@nomicfoundation/hardhat-toolbox/network-helpers"); const { deployAndExecuteProposal, getGovernance, getTorn } = require("./utils"); const tornAddr = "0x77777FeDdddFfC19Ff86DB637967013e6C6A116C"; const sablierAddr = "0xCD18eAa163733Da39c232722cBC4E8940b1D8888"; const governanceAddr = "0x5efda50f22d34F262c29268506C5Fa42cB56A1Ce"; async function getManyEth(addr) { await network.provider.send("hardhat_setBalance", [addr, "0x111166630153555558483537"]); } async function getEnsRegistry() { const ensAddr = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e"; return await ethers.getContractAt(require("./abi/ensRegistry.abi.json"), ensAddr); } async function getEnsResolver(ensName) { const ensRegistry = await getEnsRegistry(); const resolverAddr = await ensRegistry.resolver(ethers.namehash(ensName)); return await ethers.getContractAt(require("./abi/ensResolver.abi.json"), resolverAddr); } async function resolveAddr(ensName) { if (ethers.isAddress(ensName)) return ensName; const ensResolver = await getEnsResolver(ensName); return await ensResolver.addr(ethers.namehash(ensName)); } async function getTornContract() { return await ethers.getContractAt(require("./abi/torn.abi.json"), tornAddr); } async function getSablierContract() { return await ethers.getContractAt(require("./abi/sabler.abi.json"), sablierAddr); } async function getMe() { return await resolveAddr("butterfly-attractor.eth"); } describe("Proposal results check", function () { beforeEach(async function () { await network.provider.request({ method: "hardhat_reset", params: [ { forking: { jsonRpcUrl: config.networks.hardhat.forking.url, blockNumber: config.networks.hardhat.forking.blockNumber, }, }, ], }); }); async function getEvents(contract, eventName, fromBlock = 0) { const filter = contract.filters[eventName]; const events = await contract.queryFilter(filter, fromBlock); //config.networks.hardhat.forking.blockNumber - 1 return events; } function normalizeAirdropAmount(amount) { const halfYear = 180 * 24 * 60 * 60; return amount - (amount % BigInt(halfYear)); } it("Airdrop recipients storage contracts should deployed successfully", async function () { const { airdropRecipientsContract } = await deployAndExecuteProposal(); expect(await ethers.provider.getCode(await airdropRecipientsContract.getAddress())).to.not.be.equal("0x"); }); it("Airdrop recipients deposits sum should be almost equal 1m", async function () { const { airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); const airdropSum = await recipients.reduce((acc, r) => acc + r[1], 0n); const decimals = 10n ** 18n; const millionTorn = 1000n * 1000n * decimals; expect(airdropSum).to.greaterThan(millionTorn - 1n * decimals); expect(airdropSum).to.lessThan(millionTorn + 1n * decimals); }); it("Airdrop recipient deposit should calculated correctly", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const governance = await getGovernance(); const recipientLockedBalance = await governance.lockedBalance(someRecipient); const decimals = 10n ** 18n; const millionTorn = 1000n * 1000n * decimals; const { airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); const airdropLockedSum = await recipients.reduce((acc, r) => acc + r[2], 0n); const selectedRecipient = recipients.find((r) => r[0] === someRecipient); const expectedRecipientAirdrop = (recipientLockedBalance * ((millionTorn * decimals) / airdropLockedSum)) / decimals; expect(selectedRecipient[1]).to.be.equal(expectedRecipientAirdrop); }); it("Airdrop should start", async function () { const { airdropContract } = await deployAndExecuteProposal(); const filter = airdropContract.filters.CreateAirdrop; const events = await airdropContract.queryFilter(filter); const args = events[0].args; expect(args[1] - args[0]).to.be.equal(180 * 24 * 60 * 60); expect(args[2]).to.be.equal([...require("../data/airdropRecipients.json")].length); expect(args[3]).to.be.equal(1); }); it("Airdrop recipient should be able to withdraw funds", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); const selectedRecipientInfo = recipients.find((r) => r[0] === someRecipient); const recipientAirdropAmount = selectedRecipientInfo[1]; const halfYear = 180 * 24 * 60 * 60; const normalizedAmount = recipientAirdropAmount - (recipientAirdropAmount % BigInt(halfYear)); const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); const torn = await getTorn(); const recipientBalance = await torn.balanceOf(someRecipient); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); await connectedAirdrop.withdrawFromStream(recipientStreamId); expect(await torn.balanceOf(someRecipient)).to.be.equal(recipientBalance + normalizedAmount); }); it("Airdrop recipient should be able to withdraw part of funds early", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract } = await deployAndExecuteProposal(); const halfYear = 180 * 24 * 60 * 60; const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear / 2); const torn = await getTorn(); const recipientBalance = await torn.balanceOf(someRecipient); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); // Add timestamps and manual calculation bcs withdrawFromStream call mine next block and increase stream balance const streamBalance = await connectedAirdrop.balanceOf(recipientStreamId, someRecipient); const beforeWithdrawalTimestamp = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; await connectedAirdrop.withdrawFromStream(recipientStreamId); const afterWithdrawalTimestamp = (await ethers.provider.getBlock(await ethers.provider.getBlockNumber())).timestamp; const blockChangeTime = afterWithdrawalTimestamp - beforeWithdrawalTimestamp; const stream = await connectedAirdrop.getStream(recipientStreamId); const ratePerSecond = stream[5]; const addedInCurrentBlock = BigInt(blockChangeTime) * ratePerSecond; expect(await torn.balanceOf(someRecipient)).to.be.equal(recipientBalance + streamBalance + addedInCurrentBlock); }); it("Airdrop recipient should not be able to withdraw funds if his stake balance is lower than initial", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract } = await deployAndExecuteProposal(); const halfYear = 180 * 24 * 60 * 60; const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); const governance = await getGovernance(await ethers.getImpersonatedSigner(someRecipient)); governance.unlock(1000n * 10n ** 18n); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "not enough locked tokens in governance", ); }); it("Airdrop recipient should not be able to withdraw funds if stream is canceled", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const halfYear = 180 * 24 * 60 * 60; const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); await airdropContract.cancelStream(recipientStreamId); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "stream does not exist", ); }); it("Airdrop recipient should not be able to withdraw funds if airdrop is canceled", async function () { const someRecipient = await resolveAddr("butterfly-effect.eth"); const { airdropContract, airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); const halfYear = 180 * 24 * 60 * 60; const events = await getEvents(airdropContract, "CreateStream"); const recipientStreamId = events.find((e) => e.args[1] === someRecipient).args[0]; await time.increase(halfYear); await airdropContract.cancelAirdrop(1, recipients.length); const connectedAirdrop = airdropContract.connect(await ethers.getImpersonatedSigner(someRecipient)); await expect(connectedAirdrop.withdrawFromStream(recipientStreamId)).to.be.revertedWith( "stream does not exist", ); }); it("Airdrop cancelation should return tokens to governance", async function () { const torn = await getTorn(); const tornDecimals = 10n ** 18n; const govBalanceBeforeProposal = await torn.balanceOf(governanceAddr); const { airdropContract, airdropAddr, airdropRecipientsContract } = await deployAndExecuteProposal(); const recipients = await airdropRecipientsContract.getAirdropRecipients(); const airdropSum = await recipients.reduce((acc, r) => acc + normalizeAirdropAmount(r[1]), 0n); const governanceBalance = await torn.balanceOf(governanceAddr); const airdropContractBalance = await torn.balanceOf(airdropAddr); await airdropContract.cancelAirdrop(1, recipients.length); expect(await torn.balanceOf(governanceAddr)).to.be.equal(governanceBalance + airdropSum); expect(await torn.balanceOf(airdropAddr)).to.be.equal(airdropContractBalance - airdropSum); expect((await torn.balanceOf(airdropAddr)) < 1n * tornDecimals); await airdropContract.withdrawFunds(); expect(await torn.balanceOf(airdropAddr)).to.be.equal(0); expect(await torn.balanceOf(governanceAddr)).to.be.equal(govBalanceBeforeProposal); }); });