UNPKG

@vechain/vebetterdao-contracts

Version:

Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.

434 lines (433 loc) 26.4 kB
import { ethers, network } from "hardhat"; import { expect } from "chai"; import { createProposalAndExecuteIt, bootstrapAndStartEmissions, bootstrapEmissions, participateInAllocationVoting, } from "./helpers/common"; import { getOrDeployContractInstances } from "./helpers/deploy"; import { catchRevert } from "./helpers/exceptions"; import { describe, it, before } from "mocha"; import { fundTreasuryVET, fundTreasuryVTHO } from "./helpers/fundTreasury"; import { Treasury__factory } from "../typechain-types"; import { createLocalConfig } from "@repo/config/contracts/envs/local"; import { deployProxy } from "../scripts/helpers"; import { getEventName } from "./helpers/events"; import { ZERO_ADDRESS } from "./helpers"; describe("Treasury - @shard7", () => { let treasuryProxy; let b3tr; let vot3; let galaxyMember; let owner; let otherAccount; before(async () => { const config = createLocalConfig(); config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 1; const info = await getOrDeployContractInstances({ forceDeploy: true, config, }); treasuryProxy = info.treasury; owner = info.owner; otherAccount = info.otherAccount; b3tr = info.b3tr; vot3 = info.vot3; galaxyMember = info.galaxyMember; await treasuryProxy.setTransferLimitVET(ethers.parseEther("1")); await treasuryProxy.setTransferLimitToken(await b3tr.getAddress(), ethers.parseEther("1")); await treasuryProxy.setTransferLimitToken(await vot3.getAddress(), ethers.parseEther("1")); await fundTreasuryVTHO(await treasuryProxy.getAddress(), ethers.parseEther("10")); await fundTreasuryVET(await treasuryProxy.getAddress(), 10); const operatorRole = await b3tr.MINTER_ROLE(); await b3tr.grantRole(operatorRole, owner); await b3tr.mint(await treasuryProxy.getAddress(), ethers.parseEther("20")); }); describe("Initilization", () => { it("Should revert if B3TR is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, vot3 } = await getOrDeployContractInstances({ forceDeploy: false, config, }); await expect(deployProxy("Treasury", [ ZERO_ADDRESS, await vot3.getAddress(), owner.address, owner.address, owner.address, owner.address, config.TREASURY_TRANSFER_LIMIT_VET, config.TREASURY_TRANSFER_LIMIT_B3TR, config.TREASURY_TRANSFER_LIMIT_VOT3, config.TREASURY_TRANSFER_LIMIT_VTHO, ])).to.be.reverted; }); it("Should revert if VOT3 is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, b3tr } = await getOrDeployContractInstances({ forceDeploy: false, config, }); await expect(deployProxy("Treasury", [ await b3tr.getAddress(), ZERO_ADDRESS, owner.address, owner.address, owner.address, owner.address, config.TREASURY_TRANSFER_LIMIT_VET, config.TREASURY_TRANSFER_LIMIT_B3TR, config.TREASURY_TRANSFER_LIMIT_VOT3, config.TREASURY_TRANSFER_LIMIT_VTHO, ])).to.be.reverted; }); it("Should revert if admin is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, vot3, b3tr } = await getOrDeployContractInstances({ forceDeploy: false, config, }); await expect(deployProxy("Treasury", [ await b3tr.getAddress(), await vot3.getAddress(), owner.address, ZERO_ADDRESS, owner.address, owner.address, config.TREASURY_TRANSFER_LIMIT_VET, config.TREASURY_TRANSFER_LIMIT_B3TR, config.TREASURY_TRANSFER_LIMIT_VOT3, config.TREASURY_TRANSFER_LIMIT_VTHO, ])).to.be.reverted; }); }); describe("Tokens", () => { describe("VTHO", () => { it("should transfer VTHO", async () => { if (network.name == "hardhat") { return console.log("Skipping VTHO transfer test on hardhat network as hardcoded VTHO contract address in Treasury does not exist"); } const balance = await treasuryProxy.getVTHOBalance(); await expect(treasuryProxy.transferVTHO(otherAccount.address, ethers.parseEther("1"))).not.to.be.reverted; expect(await treasuryProxy.getVTHOBalance()).to.be.lessThan(balance); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferVTHO(otherAccount.address, ethers.parseEther("1"))); }); it("only governance can transfer VTHO", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferVTHO(otherAccount.address, ethers.parseEther("1"))); }); it("should revert if contract is paused", async () => { await treasuryProxy.pause(); await catchRevert(treasuryProxy.transferVTHO(otherAccount.address, ethers.parseEther("1"))); await treasuryProxy.unpause(); }); }); describe("VET", () => { it("should transfer VET", async () => { //Since timelock is the only with governance role, let's grant to a wallet to simulate await treasuryProxy.connect(owner).grantRole(await treasuryProxy.GOVERNANCE_ROLE(), owner.address); expect(await treasuryProxy.getVETBalance()).to.eql(ethers.parseEther("10")); //Transfer VET to other account using the wallet with governance role await treasuryProxy.connect(owner).transferVET(otherAccount.address, ethers.parseEther("1")); expect(await treasuryProxy.getVETBalance()).to.eql(ethers.parseEther("9")); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferVET(otherAccount.address, ethers.parseEther("11"))); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferVET(otherAccount.address, ethers.parseEther("1"))); }); it("Should revert if transfer exceeds limit", async () => { await catchRevert(treasuryProxy.transferVET(otherAccount.address, ethers.parseEther("2"))); }); it("Should be able to set transfer limit for VET", async () => { const tx = await treasuryProxy.connect(owner).setTransferLimitVET(ethers.parseEther("2")); const receipt = await tx.wait(); const name = getEventName(receipt, treasuryProxy); expect(name).to.eql("TransferLimitVETUpdated"); expect(await treasuryProxy.getTransferLimitVET()).to.eql(ethers.parseEther("2")); await treasuryProxy.transferVET(otherAccount.address, ethers.parseEther("2")); expect(await treasuryProxy.getVETBalance()).to.eql(ethers.parseEther("7")); await expect(treasuryProxy.connect(otherAccount).setTransferLimitVET(ethers.parseEther("2"))).to.be.reverted; // not admin }); }); describe("B3TR", () => { it("should transfer B3TR", async () => { expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("20")); await treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("1")); expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("19")); }); it("should convert B3TR and recieve VOT3", async () => { await treasuryProxy.convertB3TR(ethers.parseEther("10")); expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("9")); expect(await treasuryProxy.getVOT3Balance()).to.eql(ethers.parseEther("10")); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("11"))); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferB3TR(otherAccount.address, ethers.parseEther("1"))); }); it("should revert if contract is paused", async () => { await treasuryProxy.pause(); await catchRevert(treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("1"))); await treasuryProxy.unpause(); }); it("should revert if b3tr contract is paused", async () => { await b3tr.pause(); await catchRevert(treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("1"))); await b3tr.unpause(); }); it("can't convert more than balance", async () => { const balance = await treasuryProxy.getB3TRBalance(); await catchRevert(treasuryProxy.convertB3TR(balance + 1n)); }); it("should return correct address for contract", async () => { expect(await treasuryProxy.b3trAddress()).to.eql(await b3tr.getAddress()); }); it("should revert convert if contract is paused", async () => { await treasuryProxy.pause(); await catchRevert(treasuryProxy.convertB3TR(ethers.parseEther("1"))); await treasuryProxy.unpause(); }); it("Should revert if transfer exceeds limit", async () => { await catchRevert(treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("2"))); }); it("Should be able to set transfer limit", async () => { await treasuryProxy.connect(owner).setTransferLimitToken(await b3tr.getAddress(), ethers.parseEther("2")); expect(await treasuryProxy.getTransferLimitToken(await b3tr.getAddress())).to.eql(ethers.parseEther("2")); await treasuryProxy.transferB3TR(otherAccount.address, ethers.parseEther("2")); expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("7")); await expect(treasuryProxy.connect(otherAccount).setTransferLimitToken(await b3tr.getAddress(), ethers.parseEther("2"))).to.be.reverted; // not admin }); }); describe("VOT3", () => { it("should transfer VOT3", async () => { expect(await treasuryProxy.getVOT3Balance()).to.eql(ethers.parseEther("10")); await treasuryProxy.transferVOT3(otherAccount.address, ethers.parseEther("1")); expect(await treasuryProxy.getVOT3Balance()).to.eql(ethers.parseEther("9")); }); it("should convert VOT3 and recieve B3TR", async () => { await treasuryProxy.convertVOT3(ethers.parseEther("5")); expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("12")); expect(await treasuryProxy.getVOT3Balance()).to.eql(ethers.parseEther("4")); }); it("should revert if not enough converted B3TR to convert back", async () => { await catchRevert(treasuryProxy.convertVOT3(ethers.parseEther("11"))); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferVOT3(otherAccount.address, ethers.parseEther("1"))); }); it("should revert if contract is paused", async () => { await treasuryProxy.pause(); await catchRevert(treasuryProxy.convertVOT3(ethers.parseEther("1"))); await treasuryProxy.unpause(); }); it("should return correct address for contract", async () => { expect(await treasuryProxy.vot3Address()).to.eql(await vot3.getAddress()); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferVOT3(otherAccount.address, ethers.parseEther("6"))); }); it("should revert if vot3 contract is paused", async () => { await vot3.pause(); await catchRevert(treasuryProxy.transferVOT3(otherAccount.address, ethers.parseEther("1"))); await vot3.unpause(); }); it("Should revert if transfer exceeds limit", async () => { await catchRevert(treasuryProxy.transferVOT3(otherAccount.address, ethers.parseEther("2"))); }); it("Should be able to set transfer limit", async () => { await treasuryProxy.convertB3TR(ethers.parseEther("10")); const tx = await treasuryProxy .connect(owner) .setTransferLimitToken(await vot3.getAddress(), ethers.parseEther("2")); const receipt = await tx.wait(); const name = getEventName(receipt, treasuryProxy); expect(name).to.eql("TransferLimitUpdated"); expect(await treasuryProxy.getTransferLimitToken(await vot3.getAddress())).to.eql(ethers.parseEther("2")); await treasuryProxy.transferVOT3(otherAccount.address, ethers.parseEther("2")); expect(await treasuryProxy.getVOT3Balance()).to.eql(ethers.parseEther("12")); await expect(treasuryProxy.connect(otherAccount).setTransferLimitToken(await vot3.getAddress(), ethers.parseEther("2"))).to.be.reverted; // not admin }); }); describe("ERC20", () => { it("should transfer ERC20", async () => { await b3tr.mint(await treasuryProxy.getAddress(), ethers.parseEther("10")); expect(await treasuryProxy.getB3TRBalance()).to.eql(ethers.parseEther("12")); expect(await treasuryProxy.getTokenBalance(await b3tr.getAddress())).to.eql(ethers.parseEther("12")); await treasuryProxy.transferTokens(await b3tr.getAddress(), otherAccount.address, ethers.parseEther("1")); expect(await treasuryProxy.getTokenBalance(await b3tr.getAddress())).to.eql(ethers.parseEther("11")); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferTokens(await vot3.getAddress(), otherAccount.address, ethers.parseEther("6"))); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy .connect(otherAccount) .transferTokens(await vot3.getAddress(), otherAccount.address, ethers.parseEther("1"))); }); it("should revert if B3TR contract is paused", async () => { await b3tr.pause(); await catchRevert(treasuryProxy.transferTokens(await b3tr.getAddress(), otherAccount.address, ethers.parseEther("1"))); await b3tr.unpause(); }); it("Should revert if transfer exceeds limit", async () => { await catchRevert(treasuryProxy.transferTokens(await vot3.getAddress(), otherAccount.address, ethers.parseEther("6"))); }); it("Should be able to set transfer limit", async () => { await treasuryProxy.connect(owner).setTransferLimitToken(await b3tr.getAddress(), ethers.parseEther("2")); expect(await treasuryProxy.getTransferLimitToken(await b3tr.getAddress())).to.eql(ethers.parseEther("2")); await treasuryProxy.transferTokens(await b3tr.getAddress(), otherAccount.address, ethers.parseEther("2")); expect(await treasuryProxy.getTokenBalance(await b3tr.getAddress())).to.eql(ethers.parseEther("9")); await expect(treasuryProxy.connect(otherAccount).setTransferLimitToken(await b3tr.getAddress(), ethers.parseEther("2"))).to.be.reverted; // not admin }); }); describe("NFT", () => { it("should transfer NFT", async () => { // Bootstrap emissions await bootstrapEmissions(); // Should be able to free mint after participating in allocation voting await participateInAllocationVoting(otherAccount); await expect(await galaxyMember.connect(otherAccount).freeMint()).not.to.be.reverted; expect(await galaxyMember.balanceOf(otherAccount.address)).to.equal(1); await expect(await galaxyMember .connect(otherAccount) .transferFrom(otherAccount.address, await treasuryProxy.getAddress(), 1)).not.to.be.reverted; const MAGIC_ON_ERC721_RECEIVED = "0x150b7a02"; expect(await treasuryProxy.onERC721Received(owner.address, otherAccount.address, 1, ethers.toUtf8Bytes(""))).to.equal(MAGIC_ON_ERC721_RECEIVED); expect(await galaxyMember.balanceOf(await treasuryProxy.getAddress())).to.equal(1); expect(await treasuryProxy.getCollectionNFTBalance(await galaxyMember.getAddress())).to.equal(1); await expect(await treasuryProxy.transferNFT(await galaxyMember.getAddress(), otherAccount.address, 1)).not.to .be.reverted; expect(await treasuryProxy.getCollectionNFTBalance(await galaxyMember.getAddress())).to.equal(0); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy.connect(otherAccount).transferNFT(await galaxyMember.getAddress(), otherAccount.address, 1)); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferNFT(await galaxyMember.getAddress(), otherAccount.address, 1)); }); }); describe("ERC1155", () => { let erc1155; before(async () => { const erc1155ContractFactory = await ethers.getContractFactory("MyERC1155"); erc1155 = await erc1155ContractFactory.deploy(owner.address); }); it("should transfer ERC1155", async () => { await erc1155.connect(owner).mint(await treasuryProxy.getAddress(), 1, 1, new Uint8Array(0)); expect(await treasuryProxy.getERC1155TokenBalance(await erc1155.getAddress(), 1)).to.eql(1n); await treasuryProxy.transferERC1155Tokens(await erc1155.getAddress(), owner.address, 1, 1, new Uint8Array(0)); expect(await treasuryProxy.getERC1155TokenBalance(await erc1155.getAddress(), 1)).to.eql(0n); expect(await erc1155.balanceOf(owner.address, 1)).to.eql(1n); }); it("should revert if not called by GOVERNANCE_ROLE", async () => { await catchRevert(treasuryProxy .connect(otherAccount) .transferERC1155Tokens(await erc1155.getAddress(), owner.address, 1, 1, new Uint8Array(0))); }); it("should revert if not enough balance", async () => { await catchRevert(treasuryProxy.transferERC1155Tokens(await erc1155.getAddress(), owner.address, 1, 1, new Uint8Array(0))); }); it("should be able to recieve a batch of ERC1155 tokens", async () => { await erc1155.connect(owner).mintBatch(await treasuryProxy.getAddress(), [2, 3], [2, 3], new Uint8Array(0)); expect(await treasuryProxy.getERC1155TokenBalance(await erc1155.getAddress(), 2)).to.eql(2n); expect(await treasuryProxy.getERC1155TokenBalance(await erc1155.getAddress(), 3)).to.eql(3n); }); }); }); describe("UUPS", () => { it("should upgrade", async () => { const newTreasury = await ethers.getContractFactory("Treasury"); const newImplementation = await newTreasury.deploy(); const emptyBytes = new Uint8Array(0); await treasuryProxy.upgradeToAndCall(await newImplementation.getAddress(), emptyBytes); const treasury = await ethers.getContractAt("Treasury", await treasuryProxy.getAddress()); expect(await treasury.getVETBalance()).to.eql(ethers.parseEther("7")); }); it("should revert if not called by ADMIN_ROLE", async () => { const newTreasury = await ethers.getContractFactory("Treasury"); const newImplementation = await newTreasury.deploy(); const emptyBytes = new Uint8Array(0); await catchRevert(treasuryProxy.connect(otherAccount).upgradeToAndCall(await newImplementation.getAddress(), emptyBytes)); }); it("should return correct version", async () => { expect(await treasuryProxy.version()).to.eql("1"); }); it("can be initialized only once", async () => { await catchRevert(treasuryProxy.initialize(owner.address, owner.address, owner.address, owner.address, owner.address, owner.address, 1, 1, 1, 1)); }); }); describe("Pause", () => { it("should pause and unpause", async () => { expect(await treasuryProxy.hasRole(await treasuryProxy.PAUSER_ROLE(), owner.address)).to.eql(true); await treasuryProxy.pause(); expect(await treasuryProxy.paused()).to.eql(true); await treasuryProxy.unpause(); expect(await treasuryProxy.paused()).to.eql(false); }); it("should revert if not called by ADMIN_ROLE", async () => { expect(await treasuryProxy.hasRole(await treasuryProxy.PAUSER_ROLE(), otherAccount.address)).to.eql(false); await catchRevert(treasuryProxy.connect(otherAccount).pause()); await catchRevert(treasuryProxy.connect(otherAccount).unpause()); }); }); describe("Timelock", () => { let tProxy; let governor; before(async () => { const info = await getOrDeployContractInstances({ forceDeploy: true, }); governor = info.governor; const config = createLocalConfig(); tProxy = (await deployProxy("Treasury", [ await info.b3tr.getAddress(), await info.vot3.getAddress(), await info.timeLock.getAddress(), owner.address, owner.address, owner.address, config.TREASURY_TRANSFER_LIMIT_VET, config.TREASURY_TRANSFER_LIMIT_B3TR, config.TREASURY_TRANSFER_LIMIT_VOT3, config.TREASURY_TRANSFER_LIMIT_VTHO, ])); await fundTreasuryVET(await tProxy.getAddress(), 10); }); it("should execute transfer TX from proposal", async () => { const description = "Test Proposal: testing propsal for Transfer VET from tresausry"; const treasuryContractFactory = await ethers.getContractFactory("Treasury"); await bootstrapAndStartEmissions(); await governor .connect(owner) .setWhitelistFunction(await tProxy.getAddress(), tProxy.interface.getFunction("transferVET").selector, true); await createProposalAndExecuteIt(owner, otherAccount, tProxy, treasuryContractFactory, description, "transferVET", [owner.address, ethers.parseEther("1")]); expect(await tProxy.getVETBalance()).to.eql(ethers.parseEther("9")); }); it("Should be able to set VET transfer limit through proposal", async () => { const treasuryContractFactory = await ethers.getContractFactory("Treasury"); const description = "Test Proposal: testing propsal for setting VET transfer limit"; await governor .connect(owner) .setWhitelistFunction(await tProxy.getAddress(), treasuryProxy.interface.getFunction("setTransferLimitVET").selector, true); await createProposalAndExecuteIt(owner, otherAccount, tProxy, treasuryContractFactory, description, "setTransferLimitVET", [ethers.parseEther("3")]); expect(await tProxy.getTransferLimitVET()).to.eql(ethers.parseEther("3")); }); it("Should be able to set token transfer limit through proposal", async () => { const treasuryContractFactory = await ethers.getContractFactory("Treasury"); const description = "Test Proposal: testing propsal for setting token transfer limit"; await governor .connect(owner) .setWhitelistFunction(await tProxy.getAddress(), treasuryProxy.interface.getFunction("setTransferLimitToken").selector, true); await createProposalAndExecuteIt(owner, otherAccount, tProxy, treasuryContractFactory, description, "setTransferLimitToken", [await b3tr.getAddress(), ethers.parseEther("3")]); expect(await tProxy.getTransferLimitToken(await b3tr.getAddress())).to.eql(ethers.parseEther("3")); }); }); describe("Fallback", () => { it("Fallback function handles invalid calls", async () => { const nonExistentFuncSignature = "nonExistentFunction(uint256,uint256)"; const treasuryWithFakeFunction = new ethers.Contract(await treasuryProxy.getAddress(), [...Treasury__factory.createInterface().fragments, `function ${nonExistentFuncSignature}`], owner); await treasuryWithFakeFunction.nonExistentFunction(1, 1); }); }); });