From 35c3a7c5a613c8eec4c22eb0041903a47e58ccea Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Fri, 28 Jul 2023 20:04:24 +0300 Subject: [PATCH 01/11] Governance migration related constants --- PoolConstants.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/PoolConstants.ts b/PoolConstants.ts index d34f586..9e87c2a 100644 --- a/PoolConstants.ts +++ b/PoolConstants.ts @@ -10,6 +10,7 @@ export abstract class Conf { static readonly stakeRecoverFine = toNano('10'); static readonly gracePeriod = 600; static readonly sudoQuarantine = 86400; + static readonly governorQuarantine = 86400; }; export abstract class Op { @@ -127,6 +128,8 @@ export abstract class Errors { static readonly withdrawal_while_credited = 0xf800; static readonly incorrect_withdrawal_amount = 0xf801; static readonly halted = 0x9285; + static readonly governor_update_too_soon = 0xa001; + static readonly governor_update_not_matured = 0xa002; static readonly newStake = { From bf9f0e24f4017f436492be93f3d4bddb72a0d526 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Fri, 28 Jul 2023 20:05:17 +0300 Subject: [PATCH 02/11] Governor/Sudo ops --- wrappers/Pool.ts | 56 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/wrappers/Pool.ts b/wrappers/Pool.ts index e82c01d..ac845f6 100644 --- a/wrappers/Pool.ts +++ b/wrappers/Pool.ts @@ -1,4 +1,4 @@ -import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano, TupleBuilder, Dictionary, DictionaryValue } from 'ton-core'; +import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode, toNano, TupleBuilder, Dictionary, DictionaryValue, Message, storeMessage } from 'ton-core'; import { JettonMinter as AwaitedJettonMinter} from '../contracts/awaited_minter/wrappers/JettonMinter'; @@ -370,6 +370,60 @@ export class Pool implements Contract { }); } + async sendSetSudoer(provider: ContractProvider, via: Sender, sudoer: Address, value: bigint = toNano('1')) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value, + body: beginCell().storeUint(Op.governor.set_sudoer, 32) + .storeUint(1, 64) + .storeAddress(sudoer) + .endCell() + }); + } + + async sendSudoMsg(provider: ContractProvider, via: Sender, mode:number, msg: Message, query_id: bigint | number = 0) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value : toNano('1'), + body: beginCell().storeUint(Op.sudo.send_message, 32) + .storeUint(query_id, 64) + .storeUint(mode, 8) + .storeRef(beginCell().store(storeMessage(msg)).endCell()) + .endCell() + }); + } + + async sendHaltMessage(provider: ContractProvider, via: Sender, query_id: bigint | number = 0) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano('1'), + body: beginCell().storeUint(Op.halter.halt, 32) + .storeUint(query_id, 64) + .endCell() + }); + } + + async sendUnhalt(provider: ContractProvider, via: Sender, query_id: bigint | number = 0) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano('1'), + body: beginCell().storeUint(Op.governor.unhalt, 32) + .storeUint(query_id, 64) + .endCell() + }); + } + + async sendPrepareGovernanceMigration(provider: ContractProvider, via: Sender, time: number | bigint, query_id: bigint | number = 0) { + await provider.internal(via, { + sendMode: SendMode.PAY_GAS_SEPARATELY, + value: toNano('1'), + body: beginCell().storeUint(Op.governor.prepare_governance_migration, 32) + .storeUint(query_id, 64) + .storeUint(time, 48) + .endCell() + }); + } + async sendUpgrade(provider: ContractProvider, via: Sender, data: Cell | null, code: Cell | null, afterUpgrade: Cell | null) { //upgrade#96e7f528 query_id:uint64 From ed84912d9f388cf8a7d281b0f3b6b2b806d51c5e Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Fri, 28 Jul 2023 20:02:45 +0300 Subject: [PATCH 03/11] Governor roles/sudo tests --- tests/Governor.spec.ts | 389 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 tests/Governor.spec.ts diff --git a/tests/Governor.spec.ts b/tests/Governor.spec.ts new file mode 100644 index 0000000..6ca72b8 --- /dev/null +++ b/tests/Governor.spec.ts @@ -0,0 +1,389 @@ +import { Blockchain, BlockchainSnapshot, BlockchainTransaction, internal, SandboxContract, TreasuryContract } from '@ton-community/sandbox'; +import { Address, Cell, toNano, Dictionary, beginCell, Sender, SendMode, Slice } from 'ton-core'; +import { Pool } from '../wrappers/Pool'; +import { Controller } from '../wrappers/Controller'; +import { JettonMinter as DAOJettonMinter, jettonContentToCell } from '../contracts/jetton_dao/wrappers/JettonMinter'; +import { JettonWallet as PoolJettonWallet } from '../wrappers/JettonWallet'; +import '@ton-community/test-utils'; +import { compile } from '@ton-community/blueprint'; +import { Conf, Op } from "../PoolConstants"; +import { randomAddress } from '../contracts/jetton_dao/tests/utils'; +import { Errors } from '../PoolConstants'; +import { differentAddress, getRandomInt, getRandomTon } from '../utils'; +import { getMsgPrices } from '../fees'; +import { flattenTransaction } from '@ton-community/test-utils'; + +describe('Governor actions tests', () => { + let pool_code: Cell; + let controller_code: Cell; + let payout_collection: Cell; + + let dao_minter_code: Cell; + let dao_wallet_code: Cell; + let dao_vote_keeper_code: Cell; + let dao_voting_code: Cell; + + let bc: Blockchain; + let pool: SandboxContract; + let controller: SandboxContract; + let poolJetton: SandboxContract; + let deployer: SandboxContract; + + let getContractData:(smc:Address) => Promise; + let getContractCode:(smc: Address) => Promise; + + let assertExitCode:(txs: BlockchainTransaction[], exit_code: number) => void; + let sudoOpsAvailable:(via: Sender, expect_exit: number) => Promise; + let testAddr: Address; + let execCell: Cell; + + beforeAll(async () => { + bc = await Blockchain.create(); + deployer = await bc.treasury('deployer', {workchain: -1, balance: toNano("1000000000")}); + + payout_collection = await compile('PayoutNFTCollection'); + + pool_code = await compile('Pool'); + controller_code = await compile('Controller'); + + dao_minter_code = await compile('DAOJettonMinter'); + let dao_wallet_code_raw = await compile('DAOJettonWallet'); + dao_vote_keeper_code = await compile('DAOVoteKeeper'); + dao_voting_code = await compile('DAOVoting'); + + //TODO add instead of set + const _libs = Dictionary.empty(Dictionary.Keys.BigUint(256), Dictionary.Values.Cell()); + _libs.set(BigInt(`0x${dao_wallet_code_raw.hash().toString('hex')}`), dao_wallet_code_raw); + const libs = beginCell().storeDictDirect(_libs).endCell(); + bc.libs = libs; + let lib_prep = beginCell().storeUint(2,8).storeBuffer(dao_wallet_code_raw.hash()).endCell(); + dao_wallet_code = new Cell({ exotic:true, bits: lib_prep.bits, refs:lib_prep.refs}); + + const content = jettonContentToCell({type:1,uri:"https://example.com/1.json"}); + poolJetton = bc.openContract(DAOJettonMinter.createFromConfig({ + admin:deployer.address, + content, + voting_code:dao_voting_code}, + dao_minter_code)); + let poolConfig = { + pool_jetton : poolJetton.address, + pool_jetton_supply : 0n, + optimistic_deposit_withdrawals: 0n, + + sudoer : randomAddress(), + governor : deployer.address, + interest_manager : deployer.address, + halter : deployer.address, + consigliere : deployer.address, + approver : deployer.address, + + controller_code : controller_code, + pool_jetton_wallet_code : dao_wallet_code, + payout_minter_code : payout_collection, + vote_keeper_code : dao_vote_keeper_code, + }; + + pool = bc.openContract(Pool.createFromConfig(poolConfig, pool_code)); + + const poolDeployResult = await pool.sendDeploy(deployer.getSender(), toNano('11')); + + // Preparation for the post update execution test + const sendSlice = Cell.fromBase64("te6ccgEBAQEAJgAASHCAGMjLBVjPFoIQO5rKAPoCy2pwgQU5yMsfyz/J0M8WyXD7AA==").beginParse(); + /*<{ + 0 PUSHINT + 24 PUSHINT + NEWC + 6 STU + ROT + STSLICER + 1000000000 PUSHINT + STVARUINT16 + 107 STU + 0 PUSHINT + 1337 PUSHINT + NEWC + 32 STU + 64 STU + ENDC + CTOS + STSLICER + ENDC + 0 PUSHINT + SENDRAWMSG + }> + */ + testAddr = randomAddress(); + const addrSlice = beginCell().storeAddress(testAddr).asSlice(); + const pushSlice = beginCell() + .storeUint(0x8d, 8) // PUSHSLICE opcode + .storeUint(addrSlice.remainingRefs, 3) + .storeUint(Math.floor(addrSlice.remainingBits / 8), 7) + .storeSlice(addrSlice) + .storeUint(1, 1) // Padding required + .storeUint(0, 2) // (267 % 8) - 1 slice padding + + execCell = beginCell().storeSlice(pushSlice.endCell().beginParse()).storeSlice(sendSlice).endCell(); + + + getContractData = async (address: Address) => { + const smc = await bc.getContract(address); + if(!smc.account.account) + throw("Account not found") + if(smc.account.account.storage.state.type != "active" ) + throw("Atempting to get data on inactive account"); + if(!smc.account.account.storage.state.state.data) + throw("Data is not present"); + return smc.account.account.storage.state.state.data + } + getContractCode = async (address: Address) => { + const smc = await bc.getContract(address); + if(!smc.account.account) + throw("Account not found") + if(smc.account.account.storage.state.type != "active" ) + throw("Atempting to get code on inactive account"); + if(!smc.account.account.storage.state.state.code) + throw("Code is not present"); + return smc.account.account.storage.state.state.code; + } + + assertExitCode = (txs, exit_code) => { + + expect(txs).toHaveTransaction({ + exitCode: exit_code, + aborted: exit_code != 0, + success: exit_code == 0 + }); + } + sudoOpsAvailable = async (via, exp_code) => { + const testMsg = internal({from: pool.address, to: via.address!, value: toNano('1')}); + const mockCell = beginCell().storeUint(Date.now(), 256).endCell(); + // Intended to check availability only. State should be preserved + const prevState = bc.snapshot(); + let res = await pool.sendSudoMsg(via, 0, testMsg); + assertExitCode(res.transactions, exp_code); + res = await pool.sendUpgrade(via, mockCell, mockCell, mockCell); + assertExitCode(res.transactions, exp_code); + await bc.loadFrom(prevState); + } + }); + describe('Governor', () => { + let updateTime: number + let newGovernor: Address; + let newInterestManager: Address; + let newHalter: Address; + let newApprover: Address; + beforeAll(() => { + bc.now = Math.floor(Date.now() / 1000); + newGovernor = randomAddress(); + newInterestManager = randomAddress(); + newHalter = randomAddress(); + newApprover = randomAddress(); + }); + describe('Prepare migration', () => { + it('Not governor should not be able to trigger migration prep', async() => { + const notGovernor = differentAddress(deployer.address); + const updTime = bc.now! + Conf.governorQuarantine + 1; + let res = await pool.sendPrepareGovernanceMigration(bc.sender(notGovernor), updTime); + expect(res.transactions).toHaveTransaction({ + on: pool.address, + from: notGovernor, + success: false, + aborted: true, + exitCode: Errors.wrong_sender + }); + }) + it('Governor should only be able to set migration time higher than minimal quarantine time', async() => { + const prevState = bc.snapshot(); + const updTime = bc.now! + Conf.governorQuarantine; + let res = await pool.sendPrepareGovernanceMigration(deployer.getSender(), updTime); + expect(res.transactions).toHaveTransaction({ + on: pool.address, + from: deployer.address, + op: Op.governor.prepare_governance_migration, + success: false, + aborted: true, + exitCode: Errors.governor_update_too_soon + }); + + res = await pool.sendPrepareGovernanceMigration(deployer.getSender(), updTime + 1); + expect(res.transactions).not.toHaveTransaction({ + on: pool.address, + from: deployer.address, + op: Op.governor.prepare_governance_migration, + success: false, + aborted: true, + exitCode: Errors.governor_update_too_soon + }); + await bc.loadFrom(prevState); + }); + it('Governor should be able to trigger migration prep', async () => { + const poolBefore = await pool.getFullData(); + const updTime = bc.now! + Conf.governorQuarantine + getRandomInt(1, 60); + let res = await pool.sendPrepareGovernanceMigration(deployer.getSender(), updTime); + expect(res.transactions).toHaveTransaction({ + on: pool.address, + from: deployer.address, + op: Op.governor.prepare_governance_migration, + success: true + }); + const poolAfter = await pool.getFullData(); + expect(poolAfter.governorUpdateAfter).toEqual(updTime); + updateTime = updTime; + }); + }); + describe('Roles update', () => { + it('Till governor quarantine expires no one should be able to trigger roles change', async () => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.governorUpdateAfter).toEqual(updateTime); + expect(bc.now).toBeLessThan(updateTime); + + let res = await pool.sendSetRoles(deployer.getSender(), + newGovernor, + newInterestManager, + newHalter, + newApprover); + + expect(res.transactions).toHaveTransaction({ + on: pool.address, + from: deployer.address, + op: Op.governor.set_roles, + success: false, + aborted: true, + exitCode: Errors.governor_update_not_matured + }); + }); + it('After governance update quarantine expired, governor should be able to update roles', async() => { + bc.now = updateTime + 1; + const poolBefore = await pool.getFullData(); + const res = await pool.sendSetRoles(deployer.getSender(), + newGovernor, + newInterestManager, + newHalter, + newApprover); + + expect(res.transactions).toHaveTransaction({ + from: deployer.address, + on: pool.address, + success: true + }); + const poolAfter = await pool.getFullData(); + // Should reset update timer + expect(poolAfter.governorUpdateAfter).toEqual(0xffffffffffff); + expect(poolAfter.governor).toEqualAddress(newGovernor); + expect(poolAfter.interestManager).toEqualAddress(newInterestManager); + expect(poolAfter.halter).toEqualAddress(newHalter) + expect(poolAfter.approver).toEqualAddress(newApprover); + }); + }); + }); + describe('Sudoer', () => { + it('Not sudoer should not be able to use sudoer request', async() => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.governor).toEqualAddress(deployer.address); + // Make sure governor is not special role for sudoer operations + await sudoOpsAvailable(deployer.getSender(), Errors.wrong_sender); + // Not sudoer should not have access to sudoer operations + const notSudoer = bc.sender(differentAddress(poolBefore.sudoer)); + await sudoOpsAvailable(notSudoer, Errors.wrong_sender); + }); + it('Governor should be able to set sudoer', async() => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.sudoer).not.toEqualAddress(deployer.address); + const res = await pool.sendSetSudoer(deployer.getSender(), deployer.address); + const poolAfter = await pool.getFullData(); + expect(poolAfter.sudoer).toEqualAddress(deployer.address); + expect(poolAfter.sudoerSetAt).toEqual(res.transactions[1].now); + }); + it('Sudo actions should not be available till quoarantine time passes', async () => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.sudoer).toEqualAddress(deployer.address); + await sudoOpsAvailable(deployer.getSender(), Errors.sudoer.quarantine); + }); + it('Sudo ops should become available after quarantine time passed', async () => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.sudoer).toEqualAddress(deployer.address); + const curTime = bc.now ? bc.now : Math.floor(Date.now() / 1000); + + expect(curTime).toBeLessThanOrEqual(poolBefore.sudoerSetAt + Conf.sudoQuarantine); + + bc.now = poolBefore.sudoerSetAt + Conf.sudoQuarantine + 1; + await sudoOpsAvailable(deployer.getSender(), 0); + }); + it('Sudoer should be able to send arbitrary message', async() => { + const prevState = bc.snapshot(); + let msgConf = getMsgPrices(bc.config, -1); + + const poolBefore = await pool.getFullData(); + expect(poolBefore.sudoer).toEqualAddress(deployer.address); + const msgVal = getRandomTon(1, 10); + const curTime = Date.now(); + const testMsg = internal({ + from: pool.address, + to: deployer.address, + value: msgVal, + body: beginCell().storeUint(curTime, 64).endCell() + }); + + let res = await pool.sendSudoMsg(deployer.getSender(), SendMode.NONE, testMsg); + expect(res.transactions).toHaveTransaction({ + from: pool.address, + to: deployer.address, + value: msgVal - msgConf.lumpPrice + }); + await bc.loadFrom(prevState); + + // Send mode should be taken into account + res = await pool.sendSudoMsg(deployer.getSender(), SendMode.PAY_GAS_SEPARATELY, testMsg) + expect(res.transactions).toHaveTransaction({ + from: pool.address, + to: deployer.address, + value: msgVal + }); + await bc.loadFrom(prevState); + }); + it('Sudoer should be able to upgrade contract', async() => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.sudoer).toEqualAddress(deployer.address); + const prevState = bc.snapshot(); + const dataBefore = await getContractData(pool.address); + const codeBefore = await getContractData(pool.address); + const mockCell = beginCell().storeUint(Date.now(), 64).endCell(); + + const res = await pool.sendUpgrade(deployer.getSender(), mockCell, mockCell, execCell); + expect(await getContractData(pool.address)).toEqualCell(mockCell); + expect(await getContractCode(pool.address)).toEqualCell(mockCell); + + expect(res.transactions).toHaveTransaction({ + from: pool.address, + to: testAddr, + op: 1337 + }); + await bc.loadFrom(prevState); + }); + it('Upgrade should not impact code/data when if not specified', async() => { + const prevState = bc.snapshot(); + const codeBefore = await getContractCode(pool.address); + const dataBefore = await getContractData(pool.address); + const mockCell = beginCell().storeUint(Date.now(), 64).endCell(); + + let res = await pool.sendUpgrade(deployer.getSender(), mockCell, null, null); // Only data + expect(await getContractData(pool.address)).toEqualCell(mockCell); + expect(await getContractCode(pool.address)).toEqualCell(codeBefore); + await bc.loadFrom(prevState); + + res = await pool.sendUpgrade(deployer.getSender(), null, mockCell, null); // Only code + expect(await getContractData(pool.address)).toEqualCell(dataBefore); + expect(await getContractCode(pool.address)).toEqualCell(mockCell); + await bc.loadFrom(prevState); + + res = await pool.sendUpgrade(deployer.getSender(), null, null, execCell); // Only execution should be possible + expect(await getContractData(pool.address)).toEqualCell(dataBefore); + expect(await getContractCode(pool.address)).toEqualCell(codeBefore); + expect(res.transactions).toHaveTransaction({ + from: pool.address, + to: testAddr + }); + await bc.loadFrom(prevState); + }); + }); +}); From 1283d026a10fe53ba8618a8da4e4e566e27aa1e1 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 08:46:34 +0300 Subject: [PATCH 04/11] Send interest as is --- wrappers/Pool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrappers/Pool.ts b/wrappers/Pool.ts index ac845f6..5804a85 100644 --- a/wrappers/Pool.ts +++ b/wrappers/Pool.ts @@ -343,7 +343,7 @@ export class Pool implements Contract { body: beginCell() .storeUint(Op.interestManager.set_interest, 32) // op = touch .storeUint(1, 64) // query id - .storeUint(Math.floor(interest * (2**24)), 24) + .storeUint(interest, 24) .endCell(), }); } From e9c5418f2bf07815d551accc6afcd4ab74818ebd Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 08:46:58 +0300 Subject: [PATCH 05/11] setGovernanceFee handler --- wrappers/Pool.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/wrappers/Pool.ts b/wrappers/Pool.ts index 5804a85..f0566e4 100644 --- a/wrappers/Pool.ts +++ b/wrappers/Pool.ts @@ -347,6 +347,17 @@ export class Pool implements Contract { .endCell(), }); } + async sendSetGovernanceFee(provider: ContractProvider, via: Sender, fee: number | bigint, query_id: number | bigint = 1) { + await provider.internal(via, { + value: toNano('0.3'), + sendMode: SendMode.PAY_GAS_SEPARATELY, + body: beginCell() + .storeUint(Op.governor.set_governance_fee, 32) + .storeUint(query_id, 64) + .storeUint(fee, 24) + .endCell() + }); + } async sendSetRoles(provider: ContractProvider, via: Sender, governor: Address | null, From b65dae13b7c343c70f1e59c292ae20570b0c721c Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 10:03:47 +0300 Subject: [PATCH 06/11] Fix governance migration not matured error code --- PoolConstants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PoolConstants.ts b/PoolConstants.ts index 9e87c2a..33f7adb 100644 --- a/PoolConstants.ts +++ b/PoolConstants.ts @@ -129,7 +129,7 @@ export abstract class Errors { static readonly incorrect_withdrawal_amount = 0xf801; static readonly halted = 0x9285; static readonly governor_update_too_soon = 0xa001; - static readonly governor_update_not_matured = 0xa002; + static readonly governor_update_not_matured = 0xa003; static readonly newStake = { From 3747cfdcac17f701b5a8e43ed81737964155f303 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 10:04:05 +0300 Subject: [PATCH 07/11] Deposits are closed error code added --- PoolConstants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/PoolConstants.ts b/PoolConstants.ts index 33f7adb..3d75ced 100644 --- a/PoolConstants.ts +++ b/PoolConstants.ts @@ -109,6 +109,7 @@ export abstract class Errors { static readonly total_credit_too_high = 0xf103; static readonly deposit_amount_too_low = 0xf200; + static readonly depossits_are_closed = 0xf201; static readonly not_enough_TON_to_process = 0xf300; From 83a6c1d1749bdedd09b34f63e76a384d791fe973 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 10:06:26 +0300 Subject: [PATCH 08/11] Mock controller and describe based rollback --- tests/Governor.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/Governor.spec.ts b/tests/Governor.spec.ts index 6ca72b8..0345fa9 100644 --- a/tests/Governor.spec.ts +++ b/tests/Governor.spec.ts @@ -25,7 +25,7 @@ describe('Governor actions tests', () => { let bc: Blockchain; let pool: SandboxContract; - let controller: SandboxContract; + let controller: SandboxContract; let poolJetton: SandboxContract; let deployer: SandboxContract; @@ -45,6 +45,8 @@ describe('Governor actions tests', () => { pool_code = await compile('Pool'); controller_code = await compile('Controller'); + // Mock + controller = await bc.treasury('Controller'); dao_minter_code = await compile('DAOJettonMinter'); let dao_wallet_code_raw = await compile('DAOJettonWallet'); @@ -172,12 +174,17 @@ describe('Governor actions tests', () => { let newInterestManager: Address; let newHalter: Address; let newApprover: Address; + let prevState: BlockchainSnapshot; beforeAll(() => { bc.now = Math.floor(Date.now() / 1000); newGovernor = randomAddress(); newInterestManager = randomAddress(); newHalter = randomAddress(); newApprover = randomAddress(); + prevState = bc.snapshot(); + }); + afterAll(async () => { + await bc.loadFrom(prevState); }); describe('Prepare migration', () => { it('Not governor should not be able to trigger migration prep', async() => { From 768de30d8f18b843df9877ec5aaf91ab750a35f2 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 10:06:54 +0300 Subject: [PATCH 09/11] Role update tests fix --- tests/Governor.spec.ts | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/Governor.spec.ts b/tests/Governor.spec.ts index 0345fa9..e16077a 100644 --- a/tests/Governor.spec.ts +++ b/tests/Governor.spec.ts @@ -239,16 +239,16 @@ describe('Governor actions tests', () => { }); }); describe('Roles update', () => { - it('Till governor quarantine expires no one should be able to trigger roles change', async () => { + it('Till governor quarantine expires no one should be able to trigger governor role', async () => { const poolBefore = await pool.getFullData(); expect(poolBefore.governorUpdateAfter).toEqual(updateTime); expect(bc.now).toBeLessThan(updateTime); let res = await pool.sendSetRoles(deployer.getSender(), newGovernor, - newInterestManager, - newHalter, - newApprover); + null, + null, + null); expect(res.transactions).toHaveTransaction({ on: pool.address, @@ -258,15 +258,36 @@ describe('Governor actions tests', () => { aborted: true, exitCode: Errors.governor_update_not_matured }); + + res = await pool.sendSetRoles(deployer.getSender(), + null, + newInterestManager, + newHalter, + newApprover); + // Other roles update is not limited + + expect(res.transactions).toHaveTransaction({ + on: pool.address, + from: deployer.address, + success: true + }); + + const dataAfter = await pool.getFullData(); + // Should not change + expect(dataAfter.governor).toEqualAddress(deployer.address); + // Other roles should + expect(dataAfter.interestManager).toEqualAddress(newInterestManager); + expect(dataAfter.halter).toEqualAddress(newHalter); + expect(dataAfter.approver).toEqualAddress(newApprover); }); it('After governance update quarantine expired, governor should be able to update roles', async() => { bc.now = updateTime + 1; const poolBefore = await pool.getFullData(); const res = await pool.sendSetRoles(deployer.getSender(), newGovernor, - newInterestManager, - newHalter, - newApprover); + null, + null, + null); expect(res.transactions).toHaveTransaction({ from: deployer.address, From b3c69666a09c16c2b14bfa210cf920f6a0acab45 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 10:07:47 +0300 Subject: [PATCH 10/11] Governance operations tests added. --- tests/Governor.spec.ts | 195 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/tests/Governor.spec.ts b/tests/Governor.spec.ts index e16077a..05e7130 100644 --- a/tests/Governor.spec.ts +++ b/tests/Governor.spec.ts @@ -303,6 +303,201 @@ describe('Governor actions tests', () => { expect(poolAfter.approver).toEqualAddress(newApprover); }); }); + describe('Deposit settings', () => { + it('Only governor should be able to set deposit settings', async () => { + const rollBack = bc.snapshot(); + const notGovernor = differentAddress(newGovernor); + const msgVal = toNano('1'); + let res = await pool.sendSetDepositSettings(bc.sender(notGovernor), msgVal, true, true); + assertExitCode(res.transactions, Errors.wrong_sender); + res = await pool.sendSetDepositSettings(bc.sender(newGovernor), msgVal, true, true); + assertExitCode(res.transactions, 0); + await bc.loadFrom(rollBack); + }); + it('Governor should be able to set deposit settings', async() => { + const poolBefore = await pool.getFullData(); + const optimistic = !poolBefore.optimisticDepositWithdrawals; + const depoOpened = !poolBefore.depositsOpen; + const res = await pool.sendSetDepositSettings(bc.sender(newGovernor), toNano('1'), optimistic, depoOpened); + assertExitCode(res.transactions, 0); + + const poolAfter = await pool.getFullData(); + expect(poolAfter.optimisticDepositWithdrawals).toEqual(optimistic); + expect(poolAfter.depositsOpen).toEqual(depoOpened); + }); + it('Closing deposit should prevent anyone from furhter deposits', async() => { + const poolBefore = await pool.getFullData(); + const governor = bc.sender(newGovernor); + const depoAmount = getRandomTon(100000, 200000); + if(poolBefore.depositsOpen) { + await pool.sendSetDepositSettings(governor, toNano('1'), poolBefore.optimisticDepositWithdrawals, false); + } + + // Not even governor + let res = await pool.sendDeposit(governor, depoAmount); + assertExitCode(res.transactions, Errors.depossits_are_closed); + // Let's test random address too just in case + res = await pool.sendDeposit(bc.sender(randomAddress()), depoAmount); + assertExitCode(res.transactions, Errors.depossits_are_closed); + }); + }); + describe('Governance fee setting', () => { + it('Not governor should not be able to set governance fee', async () => { + const poolBefore = await pool.getFullData(); + const maxFee = (1 << 24) - 1; + const newFee = (poolBefore.governanceFee + getRandomInt(100, 200)) % maxFee; + const notGovernor = differentAddress(newGovernor); + const res = await pool.sendSetGovernanceFee(bc.sender(notGovernor), newFee); + assertExitCode(res.transactions, Errors.wrong_sender); + }); + it('Governor should be able to set governance fee', async() => { + const poolBefore = await pool.getFullData(); + const maxFee = (1 << 24) - 1; + const newFee = (poolBefore.governanceFee + getRandomInt(100, 200)) % maxFee; + const res = await pool.sendSetGovernanceFee(bc.sender(newGovernor), newFee); + assertExitCode(res.transactions, 0); + + const poolAfter = await pool.getFullData(); + expect(poolAfter.governanceFee).toEqual(newFee); + }); + }); + describe('Interest setting', () => { + it('Only interest manager should be able to set interest', async () => { + const poolBefore = await pool.getFullData(); + const maxInterest = (1 << 24) - 1; + const newInterest = (poolBefore.interestRate + getRandomInt(100, 200)) % maxInterest; + const randomUser = bc.sender(differentAddress(newInterestManager)); + const governor = bc.sender(newGovernor); + + let res = await pool.sendSetInterest(randomUser, newInterest); + + assertExitCode(res.transactions, Errors.wrong_sender); + // Makre sure those are separate roles + res = await pool.sendSetInterest(governor, newInterest); + assertExitCode(res.transactions, Errors.wrong_sender); + }); + it('Interest manager should be able to set interest', async() => { + const poolBefore = await pool.getFullData(); + const maxInterest = (1 << 24) - 1; + const newInterest = (poolBefore.interestRate + getRandomInt(100, 200)) % maxInterest; + + const res = await pool.sendSetInterest(bc.sender(newInterestManager), newInterest); + assertExitCode(res.transactions, 0); + + const poolAfter = await pool.getFullData(); + expect(poolAfter.interestRate).toEqual(newInterest); + }); + }); + describe('Halting', () => { + it('Only halter should be able to halt pool', async() => { + const notHalter = differentAddress(newHalter); + + let res = await pool.sendHaltMessage(bc.sender(notHalter)); + assertExitCode(res.transactions, Errors.wrong_sender); + // Makre sure it's separate role + res = await pool.sendHaltMessage(bc.sender(newGovernor)); + assertExitCode(res.transactions, Errors.wrong_sender); + }); + it('Halter should be able to halt pool', async () => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.halted).toBe(false); + + const res = await pool.sendHaltMessage(bc.sender(newHalter)); + assertExitCode(res.transactions, 0); + + const poolAfter = await pool.getFullData(); + expect(poolAfter.halted).toBe(true); + }); + it('Pool in halted state should prevent haltable ops', async () => { + const haltedOps = [ + // Withdraw + async () => bc.sendMessage(internal({ + from: poolJetton.address, + to: pool.address, + value: toNano('1'), + body: beginCell().storeUint(Op.pool.withdraw, 32).storeUint(0, 64).endCell() + })), + async () => pool.sendDeposit(bc.sender(randomAddress()), getRandomTon(100000, 200000)), + // Loan request + async () => bc.sendMessage(internal({ + from: controller.address, + to: pool.address, + value: toNano('1'), + body: beginCell() + .storeUint(Op.pool.request_loan, 32) + .storeUint(1, 64) + .storeCoins(toNano('100000')) + .storeCoins(toNano('100000')) + .storeUint(100, 24) + .endCell() + })), + + async () => bc.sendMessage(internal({ + from: controller.address, + to: pool.address, + value: toNano('100000'), + body: beginCell() + .storeUint(Op.pool.loan_repayment, 32) + .storeUint(1, 64) + .endCell() + })), + async () => pool.sendTouch(deployer.getSender()), + async () => pool.sendRequestControllerDeploy(bc.sender(newGovernor), toNano('1000'), 1), + async () => pool.sendDonate(bc.sender(newGovernor), toNano('1')) + ]; + + for (let cb of haltedOps) { + const res = await cb(); + expect(res.transactions).toHaveTransaction({ + success: false, + aborted: true, + exitCode: Errors.halted + }); + } + }); + it('Governance ops should be possibl when halted', async() => { + const rollBack = bc.snapshot(); + const governor = bc.sender(newGovernor); + const notHaltable = [ + async () => pool.sendSudoMsg(bc.sender(newGovernor), 0, internal({ + from: pool.address, + to: deployer.address, + value: toNano('1'), + body: beginCell().endCell() + })), + async () => pool.sendUpgrade(governor, null, null, null), + async () => pool.sendSetSudoer(governor, randomAddress()), + // We don't want halt state to change here + async () => pool.sendUnhalt(bc.sender(randomAddress())), + async () => pool.sendPrepareGovernanceMigration(governor, Math.floor(Date.now() / 1000)), + async () => pool.sendSetRoles(governor, null, null, null, null), + async () => pool.sendSetDepositSettings(governor, toNano('1'), true, true), + async () => pool.sendSetGovernanceFee(governor, 0), + async () => pool.sendSetInterest(bc.sender(newInterestManager), 0) + ]; + + for (let cb of notHaltable) { + const res = await cb(); + expect(res.transactions).not.toHaveTransaction({ + on: pool.address, + exitCode: Errors.halted + }); + } + + await bc.loadFrom(rollBack); + }); + it('Only governor should be able to unhalt', async () => { + const poolBefore = await pool.getFullData(); + expect(poolBefore.halted).toBe(true); + let res = await pool.sendUnhalt(bc.sender(newHalter)); + assertExitCode(res.transactions, Errors.wrong_sender); + res = await pool.sendUnhalt(bc.sender(newGovernor)); + assertExitCode(res.transactions, 0); + + const poolAfter = await pool.getFullData(); + expect(poolAfter.halted).toBe(false); + }); + }); }); describe('Sudoer', () => { it('Not sudoer should not be able to use sudoer request', async() => { From abd06fe70b35e9efbbfb0dbc9210d4c190e276e3 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 1 Aug 2023 15:17:58 +0300 Subject: [PATCH 11/11] Merge conflicts --- PoolConstants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PoolConstants.ts b/PoolConstants.ts index 3d75ced..2d8f422 100644 --- a/PoolConstants.ts +++ b/PoolConstants.ts @@ -9,8 +9,8 @@ export abstract class Conf { static readonly hashUpdateFine = toNano('10'); static readonly stakeRecoverFine = toNano('10'); static readonly gracePeriod = 600; - static readonly sudoQuarantine = 86400; static readonly governorQuarantine = 86400; + static readonly sudoQuarantine = 86400; }; export abstract class Op {