UNPKG

@gooddollar/goodprotocol

Version:
792 lines (635 loc) 25.6 kB
import { ethers, upgrades } from "hardhat"; import { loadFixture } from "@nomicfoundation/hardhat-network-helpers"; import { expect } from "chai"; import { GoodReserveCDai, GoodDollarMintBurnWrapper, ERC20, IGoodDollar, MultichainRouterMock } from "../../types"; import { createDAO, increaseTime } from "../helpers"; import { FormatTypes } from "@ethersproject/abi"; const BN = ethers.BigNumber; export const NULL_ADDRESS = ethers.constants.AddressZero; export const BLOCK_INTERVAL = 1; const MINTER_CAP = 1000000; const MINTER_TX_MAX = 100000; const REWARD_BPS = 300; describe("GoodDollarMintBurnWrapper", () => { let goodReserve: GoodReserveCDai; let goodDollar: IGoodDollar, genericCall, avatar, founder, minter, minterUncapped, rewarder, guardian, router, wrapperAdmin, signers, setDAOAddress, nameService, cDai, controller; before(async () => { [ founder, wrapperAdmin, minter, rewarder, guardian, minterUncapped, router, ...signers ] = await ethers.getSigners(); let { controller: ctrl, avatar: av, gd, identity, setDAOAddress: sda, setSchemes: ss, reserve, cdaiAddress, genericCall: gc, nameService: ns } = await loadFixture(createDAO); nameService = ns; genericCall = gc; cDai = cdaiAddress; avatar = av; setDAOAddress = sda; controller = ctrl; console.log("deployed dao", { founder: founder.address, gd, identity, avatar }); goodDollar = (await ethers.getContractAt("IGoodDollar", gd)) as IGoodDollar; console.log("deployed contribution, deploying reserve...", { founder: founder.address }); goodReserve = reserve as GoodReserveCDai; }); const fixture = async () => { const ictrl = await ethers.getContractAt( "Controller", controller, signers[signers.length - 1] //has scheme permissions set by createDAO() ); const wrapper = (await upgrades.deployProxy( await ethers.getContractFactory("GoodDollarMintBurnWrapper"), [wrapperAdmin.address, nameService.address], { kind: "uups" } )) as GoodDollarMintBurnWrapper; await wrapper .connect(wrapperAdmin) .grantRole(await wrapper.GUARDIAN_ROLE(), guardian.address); await goodDollar.mint(controller, 10000000); //so bps limit is significant await wrapper .connect(wrapperAdmin) .addMinter( router.address, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, false ); await wrapper .connect(wrapperAdmin) .addMinter( minter.address, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, false ); await wrapper .connect(wrapperAdmin) .addMinter(rewarder.address, 0, 0, REWARD_BPS, 0, 0, 0, true); await ictrl.registerScheme( wrapper.address, ethers.constants.HashZero, "0x00000001", avatar ); return { wrapper }; }; const fixture_withMultichain = async () => { const rf = await ethers.getContractFactory("MultichainRouterMock"); const ictrl = await ethers.getContractAt( "Controller", controller, signers[signers.length - 1] ); const wrapper = (await upgrades.deployProxy( await ethers.getContractFactory("GoodDollarMintBurnWrapper"), [wrapperAdmin.address, nameService.address], { kind: "uups" } )) as GoodDollarMintBurnWrapper; const multiChainRouter = (await rf.deploy( wrapper.address )) as MultichainRouterMock; await wrapper .connect(wrapperAdmin) .addMinter( multiChainRouter.address, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, MINTER_CAP, MINTER_TX_MAX, REWARD_BPS, false ); await setDAOAddress("MULTICHAIN_ROUTER", multiChainRouter.address); await goodDollar.mint(controller, 10000000); //so bps limit is significant return { wrapper, multiChainRouter }; }; it("should be safe for upgrades", async () => { const result = await upgrades .deployProxy( await ethers.getContractFactory("GoodDollarMintBurnWrapper"), [wrapperAdmin.address, nameService.address], { kind: "uups" } ) .catch(e => false); expect(result).not.false; }); it("should have avatar as default admin ", async () => { const { wrapper } = await loadFixture(fixture); expect(await wrapper.owner()).to.equal(avatar); expect(await wrapper.hasRole(await wrapper.DEFAULT_ADMIN_ROLE(), avatar)).to .be.true; }); it("should have admin from params as default admin ", async () => { const { wrapper } = await loadFixture(fixture); expect( await wrapper.hasRole( await wrapper.DEFAULT_ADMIN_ROLE(), wrapperAdmin.address ) ).to.be.true; }); it("should have erc20 token info", async () => { const { wrapper } = await loadFixture(fixture); expect(await wrapper.decimals()).to.equal(await goodDollar.decimals()); expect(await wrapper.name()).to.equal("GoodDollar"); expect(await wrapper.symbol()).to.equal("G$"); }); it("should update updateFrequency only by admin or guardian role", async () => { const { wrapper } = await loadFixture(fixture); expect(await wrapper.updateFrequency()).to.equal(60 * 60 * 24 * 7); //default 90 days; await expect(wrapper.setUpdateFrequency(0)).to.be.revertedWith(/role/); await expect(wrapper.connect(wrapperAdmin).setUpdateFrequency(0)).to.not .reverted; expect(await wrapper.updateFrequency()).to.equal(0); await expect(wrapper.connect(guardian).setUpdateFrequency(1)).to.not .reverted; expect(await wrapper.updateFrequency()).to.equal(1); }); it("should be able to pause roles only by admin or guardian role", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.unpause(await wrapper.PAUSE_ALL_ROLE()) ).to.be.revertedWith(/role/); await expect( wrapper.connect(wrapperAdmin).pause(await wrapper.PAUSE_BURN_ROLE()) ).to.not.reverted; await expect( wrapper.connect(guardian).pause(await wrapper.PAUSE_ALL_ROLE()) ).to.not.reverted; expect(await wrapper.paused(await wrapper.PAUSE_BURN_ROLE())).to.equal( true ); expect(await wrapper.paused(await wrapper.PAUSE_ALL_ROLE())).to.equal(true); }); it("should be able to unpause roles only by admin or guardian role", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.unpause(await wrapper.PAUSE_ALL_ROLE()) ).to.be.revertedWith(/role/); await expect( wrapper.connect(wrapperAdmin).pause(await wrapper.PAUSE_BURN_ROLE()) ).to.not.reverted; await expect( wrapper.connect(wrapperAdmin).pause(await wrapper.PAUSE_ALL_ROLE()) ).to.not.reverted; await expect( wrapper.connect(guardian).unpause(await wrapper.PAUSE_ALL_ROLE()) ).to.not.reverted; await expect( wrapper.connect(wrapperAdmin).unpause(await wrapper.PAUSE_BURN_ROLE()) ).to.not.reverted; expect(await wrapper.paused(await wrapper.PAUSE_BURN_ROLE())).to.equal( false ); expect(await wrapper.paused(await wrapper.PAUSE_ALL_ROLE())).to.equal( false ); }); it("should be able to mint only by minter role", async () => { const { wrapper } = await loadFixture(fixture); await expect(wrapper.mint(founder.address, 1000)).to.revertedWith(/role/); await expect(wrapper.connect(minter).mint(signers[0].address, 1000)).to.not .reverted; expect(await goodDollar.balanceOf(signers[0].address)).to.equal(1000); }); it("should not be able to mint when minter is paused", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.connect(guardian).pause(await wrapper.PAUSE_MINT_ROLE()) ).to.not.reverted; await expect( wrapper.connect(minter).mint(signers[0].address, 1000) ).revertedWith(/pause/); }); it("should not be able to mint when passed daily cap", async () => { const { wrapper } = await loadFixture(fixture); try { while (true) { await wrapper.connect(minter).mint(signers[0].address, MINTER_TX_MAX); } } catch (e) { expect(e.message).to.contain("daily"); } }); it("should not be able to mint when passed tx cap", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.connect(minter).mint(signers[0].address, MINTER_TX_MAX + 1) ).revertedWith(/max/); }); it("should not be able to mint when passed minter cap", async () => { const { wrapper } = await loadFixture(fixture); for (let i = 0; i < MINTER_CAP / MINTER_TX_MAX; i++) { if (i % 3 === 0) { await increaseTime(60 * 60 * 24); //pass daily cap } await wrapper.connect(minter).mint(signers[0].address, MINTER_TX_MAX); } await expect( wrapper.connect(minter).mint(signers[0].address, MINTER_TX_MAX) ).revertedWith(/minter cap/); }); xit("should not be able to mint when passed global cap", async () => { const { wrapper } = await loadFixture(fixture); await wrapper .connect(wrapperAdmin) .addMinter(minterUncapped.address, 0, 10000000000, 0, 0, 0, 0, false); for (let i = 0; i < 100000000000 / 10000000000; i++) await wrapper .connect(minterUncapped) .mint(signers[0].address, 10000000000); await expect( wrapper.connect(minterUncapped).mint(signers[0].address, 10000000000) ).revertedWith(/total mint/); }); it("should not be able to burn when passed daily cap", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(signers[0].address, 100000000); await goodDollar.connect(signers[0]).approve(wrapper.address, 100000000); try { while (true) { await wrapper.connect(router).burn(signers[0].address, MINTER_TX_MAX); } } catch (e) { expect(e.message).to.contain("daily"); } }); it("should not be able to burn when passed tx cap", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(signers[0].address, 100000000); await goodDollar.connect(signers[0]).approve(wrapper.address, 100000000); await expect( wrapper.connect(router).burn(signers[0].address, MINTER_TX_MAX + 1) ).revertedWith(/max/); }); it("should not be able to burn when passed minter cap", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(signers[0].address, 100000000); await goodDollar.connect(signers[0]).approve(wrapper.address, 100000000); for (let i = 0; i < MINTER_CAP / MINTER_TX_MAX; i++) { if (i % 3 === 0) { await increaseTime(60 * 60 * 24); //pass daily cap } await wrapper.connect(router).burn(signers[0].address, MINTER_TX_MAX); } await expect( wrapper.connect(router).burn(signers[0].address, MINTER_TX_MAX) ).revertedWith(/minter cap/); }); it("should update stats after mint", async () => { const { wrapper } = await loadFixture(fixture); await wrapper.connect(minter).mint(signers[0].address, 1000); expect(await goodDollar.balanceOf(signers[0].address)).to.eq(1000); expect(await wrapper.totalMinted()).eq(1000); const minterInfo = await wrapper.minterSupply(minter.address); expect(minterInfo.totalIn).eq(1000); await wrapper.connect(minter).mint(signers[0].address, 500); const minterInfoAfter = await wrapper.minterSupply(minter.address); expect(await wrapper.totalMinted()).eq(1500); expect(minterInfoAfter.totalIn).eq(1500); }); it("should be able to burn only by minter role", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(founder.address, 1000); await goodDollar.connect(founder).approve(wrapper.address, 1000); await expect( wrapper.connect(guardian).burn(founder.address, 1000) ).to.revertedWith(/role/); await expect(wrapper.connect(router).burn(founder.address, 1000)).to.not .reverted; expect(await goodDollar.balanceOf(founder.address)).to.equal(0); }); it("should not be able to burn when router is paused", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.connect(guardian).pause(await wrapper.PAUSE_ROUTER_ROLE()) ).to.not.reverted; await goodDollar.mint(founder.address, 1000); await goodDollar.connect(founder).approve(wrapper.address, 1000); await expect( wrapper.connect(router).burn(founder.address, 1000) ).revertedWith(/pause/); }); it("should not update stats after burn when not minted yet", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(founder.address, 1000); await goodDollar.connect(founder).approve(wrapper.address, 1000); await expect(wrapper.connect(router).burn(founder.address, 1000)).to.not .reverted; expect(await wrapper.totalMinted()).eq(0); const minterInfo = await wrapper.minterSupply(minter.address); expect(minterInfo.totalIn).eq(0); }); it("should update global stats when minter!=burner after burn when already minted", async () => { const { wrapper } = await loadFixture(fixture); await wrapper.connect(minter).mint(signers[0].address, 1000); await goodDollar.mint(founder.address, 500); await goodDollar.connect(founder).approve(wrapper.address, 500); await expect(wrapper.connect(router).burn(founder.address, 500)).to.not .reverted; expect(await wrapper.totalMinted()).eq(500); const minterInfo = await wrapper.minterSupply(minter.address); expect(minterInfo.totalIn).eq(1000); }); it("should update both global and minter stats when minter==burner after burn when already minted", async () => { const { wrapper } = await loadFixture(fixture); await wrapper .connect(wrapperAdmin) .grantRole(await wrapper.ROUTER_ROLE(), minter.address); await wrapper.connect(minter).mint(signers[0].address, 1000); await goodDollar.mint(founder.address, 500); await goodDollar.connect(founder).approve(wrapper.address, 500); await expect(wrapper.connect(minter).burn(founder.address, 500)).to.not .reverted; expect(await wrapper.totalMinted()).eq(500); const minterInfo = await wrapper.minterSupply(minter.address); expect(minterInfo.totalIn).eq(500); }); it("should reset minter total when burn amount > total", async () => { const { wrapper } = await loadFixture(fixture); await wrapper .connect(wrapperAdmin) .grantRole(await wrapper.ROUTER_ROLE(), minter.address); await wrapper.connect(minter).mint(signers[0].address, 1000); await goodDollar.mint(founder.address, 2000); await goodDollar.connect(founder).approve(wrapper.address, 2000); await expect(wrapper.connect(minter).burn(founder.address, 2000)).to.not .reverted; expect(await wrapper.totalMinted()).eq(0); const minterInfo = await wrapper.minterSupply(minter.address); expect(minterInfo.totalIn).eq(0); }); it("should update mint stats after sendOrMint", async () => { const { wrapper } = await loadFixture(fixture); await wrapper.connect(rewarder).sendOrMint(signers[0].address, 1000); const minterInfo = await wrapper.minterSupply(rewarder.address); expect(await goodDollar.balanceOf(signers[0].address)).to.eq(1000); expect(minterInfo.totalIn).eq(1000); expect(minterInfo.mintedToday).eq(1000); expect(minterInfo.totalRewards).eq(1000); expect(await wrapper.totalMinted()).eq(1000); expect(await wrapper.totalMintDebt()).eq(1000); expect(await wrapper.totalRewards()).eq(1000); }); it("should not update mint stats after sendOrMint when wrapper has G$ balance", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(wrapper.address, 1000); await wrapper.connect(rewarder).sendOrMint(signers[0].address, 1000); const minterInfo = await wrapper.minterSupply(rewarder.address); expect(await goodDollar.balanceOf(signers[0].address)).to.eq(1000); expect(minterInfo.totalIn).eq(0); expect(minterInfo.totalRewards).eq(1000); expect(minterInfo.mintedToday).eq(0); expect(await wrapper.totalMinted()).eq(0); expect(await wrapper.totalMintDebt()).eq(0); expect(await wrapper.totalRewards()).eq(1000); }); it("should perform send and mint when having partial balance for sendOrMint", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(wrapper.address, 500); await wrapper.connect(rewarder).sendOrMint(signers[0].address, 1000); const minterInfo = await wrapper.minterSupply(rewarder.address); expect(await goodDollar.balanceOf(signers[0].address)).to.eq(1000); expect(await goodDollar.balanceOf(wrapper.address)).to.eq(0); expect(minterInfo.totalIn).eq(500); expect(minterInfo.totalRewards).eq(1000); expect(minterInfo.mintedToday).eq(500); expect(await wrapper.totalMinted()).eq(500); expect(await wrapper.totalMintDebt()).eq(500); expect(await wrapper.totalRewards()).eq(1000); }); it("should reduce debt in sendOrMint", async () => { const { wrapper } = await loadFixture(fixture); await wrapper.connect(rewarder).sendOrMint(signers[0].address, 200); //200 debt const minterInfo = await wrapper.minterSupply(rewarder.address); expect(await wrapper.totalMintDebt()).eq(200); expect(await wrapper.totalRewards()).eq(200); expect(minterInfo.totalRewards).eq(200); await goodDollar.mint(wrapper.address, 1200); await wrapper.connect(rewarder).sendOrMint(signers[0].address, 1000); const minterInfoAfter = await wrapper.minterSupply(rewarder.address); expect(await wrapper.totalMintDebt()).eq(0); expect(await wrapper.totalRewards()).eq(1200); expect(minterInfoAfter.totalRewards).eq(1200); }); it("should mint just partial amount if daily limit passed in sendOrMint", async () => { const { wrapper } = await loadFixture(fixture); let minterInfo = await wrapper.minterSupply(rewarder.address); await wrapper .connect(rewarder) .sendOrMint(signers[0].address, minterInfo.dailyCapIn.add(1000)); minterInfo = await wrapper.minterSupply(rewarder.address); expect(await goodDollar.balanceOf(signers[0].address)).to.eq( minterInfo.dailyCapIn ); expect(minterInfo.totalIn).eq(minterInfo.dailyCapIn); expect(minterInfo.totalRewards).eq(minterInfo.dailyCapIn); expect(minterInfo.mintedToday).eq(minterInfo.dailyCapIn); expect(await wrapper.totalMinted()).eq(minterInfo.dailyCapIn); expect(await wrapper.totalMintDebt()).eq(minterInfo.dailyCapIn); expect(await wrapper.totalRewards()).eq(minterInfo.dailyCapIn); }); it("should reset rewarder and minter mintedToday after day passed", async () => { const { wrapper } = await loadFixture(fixture); await wrapper.connect(rewarder).sendOrMint(signers[0].address, "1000"); await wrapper.connect(minter).mint(signers[0].address, "1000"); await increaseTime(60 * 60 * 24); await expect( wrapper.connect(rewarder).sendOrMint(signers[0].address, "1001") ).to.not.reverted; await expect(wrapper.connect(minter).mint(signers[0].address, "1001")).to .not.reverted; expect(await goodDollar.balanceOf(signers[0].address)).to.eq("4002"); const minterInfo = await wrapper.minterSupply(rewarder.address); const minterInfo2 = await wrapper.minterSupply(minter.address); expect(minterInfo.mintedToday).eq(minterInfo2.mintedToday).eq("1001"); }); it("should update rewarder daily limit after updateFrequency days passed", async () => { const { wrapper } = await loadFixture(fixture); await goodDollar.mint(controller, 100000000000); const totalSupplyBeforeMint = await goodDollar.totalSupply(); let minterInfo = await wrapper.minterSupply(rewarder.address); const frequency = await wrapper.updateFrequency(); await increaseTime(frequency.toNumber()); await wrapper .connect(rewarder) .sendOrMint(signers[0].address, minterInfo.dailyCapIn.add(1000)); let minterInfoAfter = await wrapper.minterSupply(rewarder.address); expect(minterInfoAfter.dailyCapIn).gt(minterInfo.dailyCapIn); expect(minterInfoAfter.dailyCapIn).eq( totalSupplyBeforeMint.mul(REWARD_BPS).div(10000) ); //we doubled the G$ supply so bps relative to supply should be double now }); it("should not mint but not revert when rewarder passes daily limit", async () => { const { wrapper } = await loadFixture(fixture); const minterInfo = await wrapper.minterSupply(rewarder.address); await wrapper .connect(rewarder) .sendOrMint(signers[0].address, minterInfo.dailyCapIn); const tx = await ( await wrapper .connect(rewarder) .sendOrMint(signers[1].address, minterInfo.dailyCapIn) ).wait(); const sendOrMintEvent = tx.events.find(_ => _.event === "SendOrMint"); expect(await goodDollar.balanceOf(signers[1].address)).to.eq(0); expect(sendOrMintEvent.args.minted).to.eq(0); expect(sendOrMintEvent.args.sent).to.eq(0); }); it("should allow guardian to update minter limits and rewarder daily limit", async () => { const { wrapper } = await loadFixture(fixture); await expect( wrapper.setMinterCaps(minter.address, 0, 0, 0, 0, 0, 0) ).to.be.revertedWith(/role/); await expect( wrapper .connect(guardian) .setMinterCaps(minter.address, 0, 0, 50, 0, 0, 60) ).to.not.reverted; const minterInfo = await wrapper.minterSupply(minter.address); const minterOutLimits = await wrapper.minterOutLimits(minter.address); expect(minterInfo.capIn).to.eq(0); expect(minterInfo.maxIn).to.eq(0); expect(minterInfo.bpsPerDayIn).to.eq(50); expect(minterOutLimits.capOut).to.eq(0); expect(minterOutLimits.maxOut).to.eq(0); expect(minterOutLimits.bpsPerDayOut).to.eq(60); expect(minterInfo.dailyCapIn).eq( (await goodDollar.totalSupply()).mul(50).div(10000) ); expect(minterOutLimits.dailyCapOut).eq( (await goodDollar.totalSupply()).mul(60).div(10000) ); }); xit("should allow guardian to update totalMintCap", async () => { const { wrapper } = await loadFixture(fixture); await expect(wrapper.setTotalMintCap(0)).to.be.revertedWith(/role/); await expect(wrapper.connect(guardian).setTotalMintCap(0)).to.not.reverted; expect(await wrapper.totalMintCap()).to.eq(0); }); it("should support transferAndCall for multichain bridge transfer", async () => { const { wrapper, multiChainRouter } = await loadFixture( fixture_withMultichain ); await goodDollar.mint(founder.address, 100000); await goodDollar.transferAndCall( wrapper.address, 100000, ethers.utils.defaultAbiCoder.encode( ["address", "uint256"], [minter.address, "4220"] ) ); expect(await goodDollar.balanceOf(founder.address)).to.eq(0); //verify burn happened expect(await goodDollar.balanceOf(wrapper.address)).to.eq(0); //verify burn happened const events = await multiChainRouter.queryFilter( multiChainRouter.filters.AnySwap() ); expect(events[0].args.recipient).to.equal(minter.address); expect(events[0].args.chainId).to.equal(4220); }); it("should default to sender as recipient on transferAndCall if recipient=0", async () => { const { wrapper, multiChainRouter } = await loadFixture( fixture_withMultichain ); await goodDollar.mint(founder.address, 100000); await goodDollar.transferAndCall( wrapper.address, 100000, ethers.utils.defaultAbiCoder.encode( ["address", "uint256"], [ethers.constants.AddressZero, "4220"] ) ); expect(await goodDollar.balanceOf(founder.address)).to.eq(0); //verify burn happened expect(await goodDollar.balanceOf(wrapper.address)).to.eq(0); //verify burn happened const events = await multiChainRouter.queryFilter( multiChainRouter.filters.AnySwap() ); expect(events[0].args.recipient).to.equal(founder.address); expect(events[0].args.chainId).to.equal(4220); }); it("should fail transferAndCall for multichain if no chainid", async () => { const { wrapper, multiChainRouter } = await loadFixture( fixture_withMultichain ); await goodDollar.mint(founder.address, 100000); await expect( goodDollar.transferAndCall( wrapper.address, 100000, ethers.utils.defaultAbiCoder.encode( ["address", "uint"], [minter.address, 0] ) ) ).revertedWith(/chainId/); }); it("should not mint or sendOrMint to self", async () => { const { wrapper } = await loadFixture(fixture); await expect(wrapper.connect(minter).mint(wrapper.address, 1)).revertedWith( /self/ ); await expect( wrapper.connect(rewarder).sendOrMint(wrapper.address, 1) ).revertedWith(/self/); }); });