From 218a46c92f2d7fb68e949115a724867ec437ea14 Mon Sep 17 00:00:00 2001 From: EmelyanenkoK Date: Thu, 10 Aug 2023 18:45:44 +0300 Subject: [PATCH] Actualize deploy script and Readme --- Readme.md | 31 ++++---- scripts/deployPool.ts | 174 ++++++++++++++++++++++++++++++++++-------- wrappers/Pool.ts | 6 +- 3 files changed, 165 insertions(+), 46 deletions(-) diff --git a/Readme.md b/Readme.md index 8cceabe..57f2a37 100644 --- a/Readme.md +++ b/Readme.md @@ -1,10 +1,22 @@ -This documentation is organised as follows: -- short description -- list of all components (each component is isolated in separate contract) -- list of all component-to-component interfaces -- list of all multicomponent execution paths +# Liquid staking pool +Liquid Staking (LSt) is a protocol that connects TON holders of all caliber +with hardware node operators to participate in TON Blockchain validation through assets pooling. -## Description +TON holders aka *Nominators* put funds to the pool and get Pool Jettons which can be used in any DeFi protocol. +Those Jettons represent share of the pool and increase in TON value by accruing validation rewards. + +Node operators can work for pool by using it's funds as validation stake and share validation reward. + +**More info in [documentation](https://ton-ls-protocol.gitbook.io/ton-liquid-staking-protocol/).** + +## Work with code +- Clone repo with all submodules: `git clone --recurse-submodules ` +- Install dependencies (you need Node v18+): `npm install` +- Build all contracts: `npx blueprint build --all` +- Run tests: `npm test -- tests/*.ts` (standard `npx blueprint test` won't work correctly due to tests in submodules. To run those change dir to submodule and run tests from there) +- Run deploy script (carefully read it and check that it does what you want): `npx blueprint run` + +## Technical description ### Terms - elector: smart-contract which accepts stakes, conduct election, decides next active validator keys and distribute reward for validation - Сontroller: smart-contract which manage funds for stake @@ -168,10 +180,3 @@ In this scheme Payout is NFT collection and "conversion obligation" is an NFT. E When you deposit TON to pool you immediately get Deposit Bill. Later after current validation round ends and funds are released from Elector, correct poolJetton/TON ratio is discovered, amount of pool jetton corresponded to total deposits value is calculated and sent to Deposit Collection. After that Deposit Collection send *burn request* to the last minted NFT which trigger the conversion of that NFT and simulatneously sending *burn request* ton the NFT before that. Here the idea that NFTs are linked list is used to iterate through whole collection. This implementation allows processed deposits/withdrawals to be sent to other users as a whole and allows autoconversion to assets when ready. **In current implementation it is the main used mechanism** - -### Payout jettons -In this scheme Payout is jetton minter and "conversion obligation" is jettons. Each round a new payouts for deposit and withdrawal are created. - -When you deposit TON to pool you immediately get Deposit jettons. Later after current validation round ends and funds are released from Elector, correct poolJetton/TON ratio is discovered, amount of pool jetton corresponded to total deposits value is calculated and sent to Deposit minter. After that Deposit jettons can be burned to retrieve pool jettons from minter. User may burn it herself, however, for convenience special *consigliere* role is introduced which have permissions to call burn from any payout jetton wallet. If successfull (that means if distribution is already started), user gets her pool jettons and *consigliere* reimbursement for fees. That way from user perspective, in a few hours after deposit she automatically gets pool jetton. - -This implementation allows processed deposits/withdrawals to be split and sent to other users by parts, however it requires eithere centralised *consigliere* or action from user to convert payout to actual asset when ready. **This payout scheme is implemented in contract/awaited_minter/ , however it is not currently used and requires additional tests before using** diff --git a/scripts/deployPool.ts b/scripts/deployPool.ts index f986768..fb2a7d8 100644 --- a/scripts/deployPool.ts +++ b/scripts/deployPool.ts @@ -1,43 +1,111 @@ -import { Address, Cell, toNano } from 'ton-core'; -import { Pool } from '../wrappers/Pool'; +import { Address, Cell, toNano, beginCell } from 'ton-core'; +import { Pool, dataToFullConfig, poolFullConfigToCell } from '../wrappers/Pool'; +import { PoolState } from "../PoolConstants"; import { JettonMinter as DAOJettonMinter, jettonContentToCell } from '../contracts/jetton_dao/wrappers/JettonMinter'; -import { compile, NetworkProvider } from '@ton-community/blueprint'; -import {JettonWallet as PoolJettonWallet } from '../contracts/jetton_dao/wrappers/JettonWallet'; +import { compile, NetworkProvider, sleep } from '@ton-community/blueprint'; +import {JettonWallet as PoolJettonWallet } from '../wrappers/JettonWallet'; import { Controller } from '../wrappers/Controller'; +import { Librarian, LibrarianConfig } from '../wrappers/Librarian'; + + +const waitForTransaction = async (provider:NetworkProvider, address:Address, + action:string = "transaction", + curTxLt:string | null = null, + maxRetry:number = 15, + interval:number=1000) => { + let done = false; + let count = 0; + const ui = provider.ui(); + let blockNum = (await provider.api().getLastBlock()).last.seqno; + if(curTxLt == null) { + let initialState = await provider.api().getAccount(blockNum, address); + let lt = initialState?.account?.last?.lt; + curTxLt = lt ? lt : null; + } + do { + ui.write(`Awaiting ${action} completion (${++count}/${maxRetry})`); + await sleep(interval); + let newBlockNum = (await provider.api().getLastBlock()).last.seqno; + if (blockNum == newBlockNum) { + continue; + } + blockNum = newBlockNum; + const curState = await provider.api().getAccount(blockNum, address); + if(curState?.account?.last !== null){ + done = curState?.account?.last?.lt !== curTxLt; + } + } while(!done && count < maxRetry); + return done; +} export async function run(provider: NetworkProvider) { const sender = provider.sender(); const admin:Address = sender.address!; + const librarian_code = await compile('Librarian'); const pool_code = await compile('Pool'); const controller_code = await compile('Controller'); const payout_collection = await compile('PayoutNFTCollection'); const dao_minter_code = await compile('DAOJettonMinter'); - const dao_wallet_code = await compile('DAOJettonWallet'); + let dao_wallet_code_raw = await compile('DAOJettonWallet'); const dao_vote_keeper_code = await compile('DAOVoteKeeper'); const dao_voting_code = await compile('DAOVoting'); - const content = jettonContentToCell({type:1,uri:"https://gist.githubusercontent.com/EmelyanenkoK/cf435a18de72141c236218cbf3ce1102/raw/dc723a2ac22717ef0101e8a3d58b14e311d6c1c7/tuna.json?6"}); + let lib_prep = beginCell().storeUint(2,8).storeBuffer(dao_wallet_code_raw.hash()).endCell(); + const dao_wallet_code = new Cell({ exotic:true, bits: lib_prep.bits, refs:lib_prep.refs}); + + const content = jettonContentToCell({type:1,uri:"https://gist.githubusercontent.com/EmelyanenkoK/cf435a18de72141c236218cbf3ce1102/raw/dc723a2ac22717ef0101e8a3d58b14e311d6c1c7/tuna.json?v=2"}); const minter = DAOJettonMinter.createFromConfig({ admin, content, voting_code:dao_voting_code}, dao_minter_code); - let poolConfig = { - pool_jetton : minter.address, - pool_jetton_supply : 0n, - optimistic_deposit_withdrawals: 0n, - - sudoer : admin, - governor : admin, - interest_manager : admin, - halter : admin, - consigliere : admin, - approver : admin, + let poolFullConfig = { + state: PoolState.NORMAL as (0 | 1), + halted: false, // not halted + totalBalance: 0n, + poolJetton : minter.address, + poolJettonSupply : 0n, + + // empty deposits/withdrawals + depositMinter: null, + requestedForDeposit: null, + withdrawalMinter: null, + requestedForWithdrawal: null, + + // To set X% APY without compound one need to calc + // (X/100) * (round_seconds/year_seconds) * (2**24) + interestRate: 1830, + optimisticDepositWithdrawals: false, + depositsOpen: true, + + savedValidatorSetHash: 0n, + currentRound: {borrowers: null, roundId: 0, + activeBorrowers: 0n, borrowed: 0n, + expected: 0n, returned: 0n, + profit: 0n}, + prevRound: {borrowers: null, roundId: 0, + activeBorrowers: 0n, borrowed: 0n, + expected: 0n, returned: 0n, + profit: 0n}, + + minLoanPerValidator: toNano('10000'), + maxLoanPerValidator: toNano('700000'), + + // To set X% put X*(2**24) here + governanceFee: 2516582, + + sudoer : Address.parse("EQDIeMe7NaJ_tvSMmv_sc--fYie_qUtXTKZgqD7h63JgwLtv"), + sudoerSetAt: 0, + governor : Address.parse("EQDYosEog79D4wBvk5QTBhsaXN1yrj96yhop1UYtQZaRwU5w"), + governorUpdateAfter: 0xffffffffffff, + interest_manager : Address.parse("EQBhTMRnu4ZpYvNuv7E7S_T6eOQQhxLLY5jFsJ9su1N8L8E9"), + halter : Address.parse("EQDzykJAVXoLdBT7gpJuk7taV1t47uviG1TkQUCCZQp4fD3S"), + approver : Address.parse("EQAWPbtEv-ol2HUv26cBUDFRvcSDhRRVxmrnYgXVYf2Y2Aoc"), controller_code : controller_code, pool_jetton_wallet_code : dao_wallet_code, @@ -45,7 +113,16 @@ export async function run(provider: NetworkProvider) { vote_keeper_code : dao_vote_keeper_code, }; - const pool = provider.open(Pool.createFromConfig(poolConfig, pool_code)); + //deploy or use existing librarian + //const librarian = provider.open(Librarian.createFromConfig({librarianId:0n}, librarian_code)); + //console.log("Librarian address:", librarian.address); + //await librarian.sendDeploy(provider.sender(), toNano("1")); + //await waitForTransaction(provider, librarian.address, "Librarian deploy"); + const librarian = provider.open(Librarian.createFromAddress(Address.parse("Ef9ymVquxBIMq3rheG_AzE4WQ7bUGQptF151_yJoEwVyJPZi"))); + await librarian.sendAddLibrary(provider.sender(), dao_wallet_code_raw); + await waitForTransaction(provider, librarian.address, "dao_wallet_code_raw registering"); + + const pool = provider.open(Pool.createFromFullConfig(poolFullConfig, pool_code)); // Deployment scheme: // 1. Deploy DAO Minter with wallet as admin @@ -53,28 +130,61 @@ export async function run(provider: NetworkProvider) { // 3. Transfer adminship of DAO Minter to Pool const poolJetton = provider.open(minter); - /*await poolJetton.sendDeploy(provider.sender(), toNano("0.1")); + await poolJetton.sendDeploy(provider.sender(), toNano("0.1")); await provider.waitForDeploy(poolJetton.address); await pool.sendDeploy(provider.sender(), toNano("11")); await provider.waitForDeploy(pool.address); await poolJetton.sendChangeAdmin(provider.sender(), pool.address); - await pool.sendDeposit(provider.sender(), toNano("10")); - await pool.sendDeposit(provider.sender(), toNano("10")); - await pool.sendSetDepositSettings(provider.sender(), toNano("10"), true, true);*/ - await pool.sendDeposit(provider.sender(), toNano("50000")); + await waitForTransaction(provider, poolJetton.address, "transfer adminship of DAO Minter to Pool"); + // Pool can start in pessimistic mode an switch during the round + /*await pool.sendDeposit(provider.sender(), toNano("2")); + await waitForTransaction(provider, pool.address, "pessimistic deposit"); + await pool.sendDeposit(provider.sender(), toNano("3")); + await waitForTransaction(provider, pool.address, "pessimistic deposit 2"); + */ + await pool.sendSetDepositSettings(provider.sender(), toNano("1"), true, true); + await waitForTransaction(provider, pool.address, "set optimistic deposit settings"); + + await pool.sendDeposit(provider.sender(), toNano("100")); + await waitForTransaction(provider, pool.address, "optimistic deposit"); + await pool.sendDonate(provider.sender(), toNano("1")); //compensate round finalize fee + await waitForTransaction(provider, pool.address, "donation"); + await pool.sendDeposit(provider.sender(), toNano("100")); + await waitForTransaction(provider, pool.address, "optimistic deposit 2"); + + // For manual pool rotation + //await pool.sendTouch(provider.sender()); + + // For manual controller managing + //let controller = provider.open(Controller.createFromAddress(Address.parse(" ==INSERT HERE== "))); + //await controller.sendUpdateHash(provider.sender()); + //await controller.sendApprove(provider.sender()); + //await controller.sendTopUp(provider.sender(), toNano('10000')); + + // For governor + //await pool.sendSetRoles(provider.sender(), null, Address.parse(" ==INSERT HERE== "), null) + //await pool.sendSetInterest(provider.sender(), 0.0005); + + // For user + //let userWallet = provider.open(PoolJettonWallet.createFromAddress(Address.parse(" ==INSERT HERE== "))); + //await userWallet.sendBurnWithParams(provider.sender(), toNano("1.0"), toNano("100000"), sender.address!, false, false); + //await new Promise(f => setTimeout(f, 10000)); + //await userWallet.sendTransfer(provider.sender(), toNano("1.0"), toNano("2"), Address.parse(" ==INSERT HERE== "), sender.address!, null, toNano("0.8"), null); + + // For sudoer /* - const controller = provider.open(Controller.createFromAddress(Address.parse("Ef-b3l6zM85S40n4bgE7v1kzk2jkNdHK-OhDmYK4bRffy8bK"))); - await controller.sendApprove(provider.sender()); + // How to "safely" update pool data: + // It is expedient to halt before and unhalt after + let fullData = await pool.getFullDataRaw(); + let newPoolConfig = dataToFullConfig(fullData); + //update data here + newPoolConfig.controller_code = controller_code; + newPoolConfig.payout_minter_code = payout_collection; + let storage = poolFullConfigToCell(newPoolConfig); + await pool.sendUpgrade(provider.sender(), storage, pool_code, null); */ - //let userWallet = provider.open(PoolJettonWallet.createFromAddress(Address.parse("EQCV-RIjUm_ykbZvJzimBL9HI1R8eRl8c4U6ImWfrFYoYDHL"))); - //await userWallet.sendBurn(provider.sender(), toNano("1"), toNano("134999"), sender.address!, null); - /*const pool = provider.open(Pool.createFromConfig({}, pool_code)); - await pool.sendDeploy(provider.sender(), toNano('0.05')); - await provider.waitForDeploy(pool.address); - */ - // run methods on `pool` } diff --git a/wrappers/Pool.ts b/wrappers/Pool.ts index 5df173a..e965b9b 100644 --- a/wrappers/Pool.ts +++ b/wrappers/Pool.ts @@ -12,7 +12,6 @@ export type PoolConfig = { governor: Address; interest_manager: Address; halter: Address; - consigliere: Address; approver: Address; controller_code: Cell; @@ -266,6 +265,11 @@ export class Pool implements Contract { const init = { code, data }; return new Pool(contractAddress(workchain, init), init); } + static createFromFullConfig(config: PoolFullConfig, code: Cell, workchain = 0) { + const data = poolFullConfigToCell(config); + const init = { code, data }; + return new Pool(contractAddress(workchain, init), init); + } async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) { await provider.internal(via, {