import { ScillaContract, initZilliqa, setAccount } from "hardhat-scilla-plugin"; import { Value } from "@zilliqa-js/contract"; import { assert, expect } from "chai"; import hre from "hardhat"; import { loadZilliqaHardhatObject } from "hardhat-scilla-plugin/dist/src/ZilliqaHardhatObject"; import * as zcrypto from "@zilliqa-js/crypto"; const newpk = zcrypto.schnorr.generatePrivateKey(); // these are all isolated server genesis accounts; if you're using devnet, // you'll need to find three accounts and put their private keys here. const adminpk = "db11cfa086b92497c8ed5a4cc6edb3a5bfe3a640c43ffb9fc6aa0873c56f2ee3"; const userpk = "e53d1c3edaffc7a7bab5418eb836cf75819a82872b4a1a0f1c7fcf5c3e020b89"; const privateKeys = [adminpk, userpk]; const network_url = "http://localhost:5555"; const chain_id = 1; // 222; const setup = initZilliqa(network_url, chain_id, privateKeys); const adminaddress = zcrypto.getAddressFromPrivateKey(adminpk).toLowerCase(); const useraddress = zcrypto.getAddressFromPrivateKey(userpk).toLowerCase(); const adminpubkey = zcrypto.getPubKeyFromPrivateKey(adminpk); const userpubkey = zcrypto.getPubKeyFromPrivateKey(userpk); describe("BurnAnyTokenZRC2Test", () => { var burncontract: ScillaContract; var zrc2contract: ScillaContract; beforeEach(async () => { setAccount(0); burncontract = await hre.deployScillaContract( "BurnTokensAnyZRC2", adminaddress ); zrc2contract = await hre.deployScillaContract( "FungibleToken", adminaddress, "TestToken", "TT", "0", "100000" ); }); it("Should deploy correctly", async () => { expect(burncontract.address).to.be.properAddress; expect(zrc2contract.address).to.be.properAddress; }); it("Should be possible to set a burn allowance", async () => { setAccount(1); // This asks us to set up a burn allowance for account 1 from zrc2contract.address for 100 tokens { await burncontract.UpdateBurnAllowance(zrc2contract.address, 100); const allowed_burn = (await burncontract.allow_burn_tokens())[ zrc2contract.address!.toLowerCase() ][useraddress]; assert(allowed_burn == 100); } // Now set it back to 0. { await burncontract.UpdateBurnAllowance(zrc2contract.address, 0); const allowed_burn = (await burncontract.allow_burn_tokens())[ zrc2contract.address!.toLowerCase() ][useraddress]; assert(allowed_burn == 0); } }); it("Should be possible to burn tokens", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); await burncontract.ChangeBurnCancelBlocks(0); setAccount(1); // We'll check that the events fire properly here so as not to have another test // case (and thus speed up tests) { let tx = await burncontract.UpdateBurnAllowance( zrc2contract.address, 100 ); expect(tx).to.eventLogWithParams( "UpdateBurnAllowanceSuccess", { type: "ByStr20", value: zrc2contract.address.toLowerCase(), vname: "token_address", }, { type: "Uint128", value: { _hex: "0x64", _isBigNumber: true }, vname: "token_amount", }, { type: "ByStr20", value: useraddress, vname: "updated_by" } ); } await zrc2contract.Transfer(burncontract.address!, 100); // Burns can be finalised by anyone // @todo when we have ZIL transfers, make this a separate account - can't at the // moment 'cos it won't have enough gas. setAccount(0); { let tx = await burncontract.FinaliseBurn( zrc2contract.address, useraddress ); expect(tx).to.eventLogWithParams( "TokenBurnConfirmation", { type: "ByStr20", value: zrc2contract.address.toLowerCase(), vname: "token_address", }, { type: "Uint128", value: { _hex: "0x64", _isBigNumber: true }, vname: "token_amount", }, { type: "ByStr20", value: useraddress, vname: "burnt_by" } ); } const pending_burn = await burncontract.pending_burn(); const balances = await zrc2contract.balances(); assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 0, "The user should have no tokens"); assert( balances[burncontract.address!.toLowerCase()] == 100, "The contract should have 100 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); }); it("Should be possible to undo your burn before the timeout expires", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); let burnCancelTx = await burncontract.ChangeBurnCancelBlocks(20); expect(burnCancelTx).to.eventLogWithParams("ChangeBurnCancelBlocks", { type: "Uint32", vname: "new_cancel_blocks", value: 20, }); setAccount(1); await burncontract.UpdateBurnAllowance(zrc2contract.address, 100); await zrc2contract.Transfer(burncontract.address!, 100); let burnTx = await burncontract.CancelBurn(zrc2contract.address); expect(burnTx).to.eventLogWithParams( "CancelBurn", { type: "ByStr20", value: zrc2contract.address.toLowerCase(), vname: "token", }, { type: "ByStr20", value: hre.zilliqa.getAccounts()[1].address.toLowerCase(), vname: "sender", }, { type: "Uint128", vname: "token_amount", value: { _hex: "0x64", _isBigNumber: true, }, }, { type: "Bool", vname: "success", value: true, } ); const pending_burn = await burncontract.pending_burn(); const balances = await zrc2contract.balances(); assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 100, "The user should have 100 tokens"); assert( balances[burncontract.address!.toLowerCase()] == 0, "The contract should have 100 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); // Try to finalise the burn... // Burns can be actioned by anyone setAccount(0); await burncontract.FinaliseBurn(zrc2contract.address, useraddress); assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 100, "The user should have 100 tokens"); assert( balances[burncontract.address!.toLowerCase()] == 0, "The contract should have 100 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); }); it("Should not be possible to undo a burn after the timeout expires", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); // This is a hack because it is hard (and takes a long time) to allow blocks to pass. await burncontract.ChangeBurnCancelBlocks(0); setAccount(1); await burncontract.UpdateBurnAllowance(zrc2contract.address, 100); await zrc2contract.Transfer(burncontract.address!, 100); { let burnTx = await burncontract.CancelBurn(zrc2contract.address); expect(burnTx).to.eventLogWithParams( "CancelBurn", { type: "ByStr20", value: zrc2contract.address.toLowerCase(), vname: "token", }, { type: "ByStr20", value: hre.zilliqa.getAccounts()[1].address.toLowerCase(), vname: "sender", }, { type: "Uint128", vname: "token_amount", value: { _hex: "0x00", _isBigNumber: true, }, }, { type: "Bool", vname: "success", value: false, } ); } const pending_burn = await burncontract.pending_burn(); const balances = await zrc2contract.balances(); assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 0, "The user should have no tokens"); assert( balances[burncontract.address!.toLowerCase()] == 100, "The contract should have 100 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); }); it("Should be possible to burn via allowances", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); await burncontract.ChangeBurnCancelBlocks(0); // This is rather dodgy, because we're reusing the admin address. // @todo when we have transfers, use a second user address for this. setAccount(1); await zrc2contract.IncreaseAllowance(adminaddress, 100); await burncontract.UpdateBurnAllowance(zrc2contract.address, 100); // Now the allowance is set up, we should be able to burn the tokens by // sending them via the allowance, setAccount(0); await zrc2contract.TransferFrom(useraddress, burncontract.address, 100); await burncontract.FinaliseBurn(zrc2contract.address, useraddress); const pending_burn = await burncontract.pending_burn(); const balances = await zrc2contract.balances(); // This should have worked .. assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 0, "The user should have no tokens"); assert( balances[burncontract.address!.toLowerCase()] == 100, "The contract should have 100 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); }); it("Should not be possible to burn more tokens than the allowance", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); await burncontract.ChangeBurnCancelBlocks(0); setAccount(1); // We'll check that the events fire properly here so as not to have another test // case (and thus speed up tests) { let tx = await burncontract.UpdateBurnAllowance(zrc2contract.address, 50); expect(tx).to.eventLogWithParams( "UpdateBurnAllowanceSuccess", { type: "ByStr20", value: zrc2contract.address.toLowerCase(), vname: "token_address", }, { type: "Uint128", value: { _hex: "0x32", _isBigNumber: true }, vname: "token_amount", }, { type: "ByStr20", value: useraddress, vname: "updated_by" } ); } { let tx = await zrc2contract.Transfer(burncontract.address!, 100); expect(tx).to.nested.include({ "receipt.accepted": false }); expect(tx).to.nested.include({ "receipt.errors.1[0]": 7 }); } // Burn finalisation should still work. setAccount(0); { await burncontract.FinaliseBurn(zrc2contract.address, useraddress); } // The user should still have their tokens const pending_burn = await burncontract.pending_burn(); const balances = await zrc2contract.balances(); assert( pending_burn[useraddress] == null, "The user should have no pending burns" ); assert(balances[useraddress] == 100, "The user should have 100 tokens"); // 0 - in fact, this will be undefined assert( balances[burncontract.address!.toLowerCase()] === undefined, "The contract should have 0 tokens" ); assert( balances[adminaddress] == 99900, "The admin must have all unburned tokens" ); }); it("Should be possible to hand over control of the contract", async () => { setAccount(0); // First, check that you can abort a failed ownership request. await burncontract.SetContractOwnershipRecipient(zrc2contract.address); setAccount(1); // We should fail to take ownership.. { let tx = await burncontract.AcceptContractOwnership(); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } // Now do it right .. setAccount(0); await burncontract.SetContractOwnershipRecipient(useraddress); setAccount(1); await burncontract.AcceptContractOwnership(); const owner = await burncontract.contract_owner(); const recipient = await burncontract.contract_ownership_recipient(); assert(owner === useraddress.toLowerCase()); assert(recipient === "0x0000000000000000000000000000000000000000"); }); it("Should be possible to pause a single ZRC2", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); // You can only pause if you're the owner. setAccount(1); { let tx = await burncontract.Pause(zrc2contract.address); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } // Set up some allowances so that we might succeed later on. await burncontract.UpdateBurnAllowance(zrc2contract.address, 50); await zrc2contract.IncreaseAllowance(adminaddress, 50); { let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); // Annoyingly, can't check this with current hardhat-scilla-plugin expect(paused["constructor"]).to.equal("False"); expect(paused_zrc2).to.be.empty; } { // .. so be the owner setAccount(0); await burncontract.Pause(zrc2contract.address); let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); let zca = zrc2contract.address.toLowerCase(); expect(paused_zrc2[zca]["constructor"]).to.equal("True"); } setAccount(1); { let tx = await burncontract.UpdateBurnAllowance( zrc2contract.address, 100 ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } { let tx = await zrc2contract.Transfer(burncontract.address!, 50); //console.log(`xTy ${JSON.stringify(tx)}`); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.1[0]": 7 }); } setAccount(0); { let tx = await zrc2contract.TransferFrom( useraddress, burncontract.address, 50 ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.1[0]": 7 }); } setAccount(1); // Cancel is in fact allowed when a particular ZRC2 is paused. // See the README.md { let tx = await burncontract.CancelBurn(zrc2contract.address); expect(tx).to.nested.include({ "receipt.success": true }); } { let tx = await burncontract.FinaliseBurn( zrc2contract.address, useraddress ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } // You need to be admin to unpause setAccount(1); { let tx = await burncontract.UnPause(zrc2contract.address); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } setAccount(0); await burncontract.UnPause(zrc2contract.address); { let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); // Annoyingly, can't check this with current hardhat-scilla-plugin expect(paused["constructor"]).to.equal("False"); expect(paused_zrc2).to.be.empty; } }); it("Should be possible to pause everything", async () => { setAccount(0); await zrc2contract.Transfer(useraddress, 100); // You can only pause if you're the owner. setAccount(1); { let tx = await burncontract.PauseAll(); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } // Set up some allowances so that we might succeed later on. await burncontract.UpdateBurnAllowance(zrc2contract.address, 50); await zrc2contract.IncreaseAllowance(adminaddress, 50); { let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); // Annoyingly, can't check this with current hardhat-scilla-plugin expect(paused["constructor"]).to.equal("False"); expect(paused_zrc2).to.be.empty; } { // .. so be the owner setAccount(0); await burncontract.PauseAll(); let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); let zca = zrc2contract.address.toLowerCase(); expect(paused["constructor"]).to.equal("True"); expect(paused_zrc2).to.be.empty; } setAccount(1); { let tx = await burncontract.UpdateBurnAllowance( zrc2contract.address, 100 ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } { let tx = await zrc2contract.Transfer(burncontract.address!, 50); //console.log(`xTy ${JSON.stringify(tx)}`); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.1[0]": 7 }); } setAccount(0); { let tx = await zrc2contract.TransferFrom( useraddress, burncontract.address, 50 ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.1[0]": 7 }); } setAccount(1); { let tx = await burncontract.CancelBurn(zrc2contract.address); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } { let tx = await burncontract.FinaliseBurn( zrc2contract.address, useraddress ); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } // You need to be admin to unpause setAccount(1); { let tx = await burncontract.UnPause(zrc2contract.address); expect(tx).to.nested.include({ "receipt.success": false }); expect(tx).to.nested.include({ "receipt.errors.0[0]": 7 }); } setAccount(0); await burncontract.UnPauseAll(); { let paused = await burncontract.paused(); let paused_zrc2 = await burncontract.paused_zrc2(); // Annoyingly, can't check this with current hardhat-scilla-plugin expect(paused["constructor"]).to.equal("False"); expect(paused_zrc2).to.be.empty; } }); });