From db6fd4b235dda2835dbd2b4cd2e4d8bde9e184c7 Mon Sep 17 00:00:00 2001 From: Trinketer22 Date: Tue, 20 Jun 2023 11:19:40 +0300 Subject: [PATCH] Integrational tests --- PoolConstants.ts | 1 + elector/config-code.fc | 643 +++++++++++++++++++ elector/elector-code.fc | 1187 +++++++++++++++++++++++++++++++++++ tests/Integrational.spec.ts | 172 +++++ utils.ts | 67 ++ wrappers/Config.compile.ts | 5 + wrappers/Config.ts | 9 + wrappers/ConfigTest.ts | 24 + wrappers/Elector.compile.ts | 5 + wrappers/Elector.ts | 126 ++++ wrappers/ElectorTest.ts | 60 ++ 11 files changed, 2299 insertions(+) create mode 100644 elector/config-code.fc create mode 100644 elector/elector-code.fc create mode 100644 tests/Integrational.spec.ts create mode 100644 wrappers/Config.compile.ts create mode 100644 wrappers/Config.ts create mode 100644 wrappers/ConfigTest.ts create mode 100644 wrappers/Elector.compile.ts create mode 100644 wrappers/Elector.ts create mode 100644 wrappers/ElectorTest.ts diff --git a/PoolConstants.ts b/PoolConstants.ts index 1eabd07..7bd106a 100644 --- a/PoolConstants.ts +++ b/PoolConstants.ts @@ -4,6 +4,7 @@ export abstract class Conf { static readonly electorOpValue = toNano('1.03'); static readonly minStorage = toNano('2'); static readonly depositFee = toNano('0.25'); + static readonly poolDepositFee = toNano('1'); static readonly withdrawlFee = toNano('0.25'); static readonly minStake = toNano('50000'); static readonly hashUpdateFine = toNano('10'); diff --git a/elector/config-code.fc b/elector/config-code.fc new file mode 100644 index 0000000..a638144 --- /dev/null +++ b/elector/config-code.fc @@ -0,0 +1,643 @@ +;; Simple configuration smart contract + +() set_conf_param(int index, cell value) impure { + var cs = get_data().begin_parse(); + var cfg_dict = cs~load_ref(); + cfg_dict~idict_set_ref(32, index, value); + set_data(begin_cell().store_ref(cfg_dict).store_slice(cs).end_cell()); +} + +(cell, int, int, cell) load_data() inline { + var cs = get_data().begin_parse(); + var res = (cs~load_ref(), cs~load_uint(32), cs~load_uint(256), cs~load_dict()); + cs.end_parse(); + return res; +} + +() store_data(cfg_dict, stored_seqno, public_key, vote_dict) impure inline { + set_data(begin_cell() + .store_ref(cfg_dict) + .store_uint(stored_seqno, 32) + .store_uint(public_key, 256) + .store_dict(vote_dict) + .end_cell()); +} + +;; (min_tot_rounds, max_tot_rounds, min_wins, max_losses, min_store_sec, max_store_sec, bit_price, cell_price) +_ parse_vote_config(cell c) inline { + var cs = c.begin_parse(); + throw_unless(44, cs~load_uint(8) == 0x36); + var res = (cs~load_uint(8), cs~load_uint(8), cs~load_uint(8), cs~load_uint(8), cs~load_uint(32), cs~load_uint(32), cs~load_uint(32), cs~load_uint(32)); + cs.end_parse(); + return res; +} + +;; cfg_vote_setup#91 normal_params:^ConfigProposalSetup critical_params:^ConfigProposalSetup = ConfigVotingSetup; +_ get_vote_config_internal(int critical?, cell cparam11) inline_ref { + var cs = cparam11.begin_parse(); + throw_unless(44, cs~load_uint(8) == 0x91); + if (critical?) { + cs~load_ref(); + } + return parse_vote_config(cs.preload_ref()); +} + +_ get_vote_config(int critical?) inline { + return get_vote_config_internal(critical?, config_param(11)); +} + +(int, int) check_validator_set(cell vset) { + var cs = vset.begin_parse(); + throw_unless(9, cs~load_uint(8) == 0x12); ;; validators_ext#12 only + int utime_since = cs~load_uint(32); + int utime_until = cs~load_uint(32); + int total = cs~load_uint(16); + int main = cs~load_uint(16); + throw_unless(9, main > 0); + throw_unless(9, total >= main); + return (utime_since, utime_until); +} + +() send_answer(addr, query_id, ans_tag, mode) impure { + ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 + send_raw_message(begin_cell() + .store_uint(0x18, 6) + .store_slice(addr) + .store_uint(0, 5 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(ans_tag, 32) + .store_uint(query_id, 64) + .end_cell(), mode); +} + +() send_confirmation(addr, query_id, ans_tag) impure inline { + return send_answer(addr, query_id, ans_tag, 64); +} + +() send_error(addr, query_id, ans_tag) impure inline { + return send_answer(addr, query_id, ans_tag, 64); +} + +;; forward a message to elector smart contract to make it upgrade its code +() change_elector_code(slice cs) impure { + var dest_addr = config_param(1).begin_parse().preload_uint(256); + var query_id = now(); + send_raw_message(begin_cell() + .store_uint(0xc4ff, 17) + .store_uint(dest_addr, 256) + .store_grams(1 << 30) ;; ~ 1 Gram (will be returned back) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(0x4e436f64, 32) ;; action + .store_uint(query_id, 64) + .store_slice(cs) + .end_cell(), 0); +} + +() after_code_upgrade(slice param, cont old_code) impure method_id(1666) { +} + +_ perform_action(cfg_dict, public_key, action, cs) inline_ref { + if (action == 0x43665021) { + ;; change one configuration parameter + var param_index = cs~load_int(32); + var param_value = cs~load_ref(); + cs.end_parse(); + cfg_dict~idict_set_ref(32, param_index, param_value); + return (cfg_dict, public_key); + } elseif (action == 0x4e436f64) { + ;; change configuration smart contract code + var new_code = cs~load_ref(); + set_code(new_code); + var old_code = get_c3(); + set_c3(new_code.begin_parse().bless()); + after_code_upgrade(cs, old_code); + throw(0); + return (cfg_dict, public_key); + } elseif (action == 0x50624b21) { + ;; change configuration master public key + public_key = cs~load_uint(256); + cs.end_parse(); + return (cfg_dict, public_key); + } elseif (action == 0x4e43ef05) { + ;; change election smart contract code + change_elector_code(cs); + return (cfg_dict, public_key); + } else { + throw_if(32, action); + return (cfg_dict, public_key); + } +} + +(cell, int, cell) get_current_vset() inline_ref { + var vset = config_param(34); + var cs = begin_parse(vset); + ;; validators_ext#12 utime_since:uint32 utime_until:uint32 + ;; total:(## 16) main:(## 16) { main <= total } { main >= 1 } + ;; total_weight:uint64 + throw_unless(40, cs~load_uint(8) == 0x12); + cs~skip_bits(32 + 32 + 16 + 16); + var (total_weight, dict) = (cs~load_uint(64), cs~load_dict()); + cs.end_parse(); + return (vset, total_weight, dict); +} + +(slice, int) get_validator_descr(int idx) inline_ref { + var (vset, total_weight, dict) = get_current_vset(); + var (value, _) = dict.udict_get?(16, idx); + return (value, total_weight); +} + +(int, int) unpack_validator_descr(slice cs) inline { + ;; ed25519_pubkey#8e81278a pubkey:bits256 = SigPubKey; + ;; validator#53 public_key:SigPubKey weight:uint64 = ValidatorDescr; + ;; validator_addr#73 public_key:SigPubKey weight:uint64 adnl_addr:bits256 = ValidatorDescr; + throw_unless(41, (cs~load_uint(8) & ~ 0x20) == 0x53); + throw_unless(41, cs~load_uint(32) == 0x8e81278a); + return (cs~load_uint(256), cs~load_uint(64)); +} + +;; cfg_proposal#f3 param_id:int32 param_value:(Maybe ^Cell) if_hash_equal:(Maybe uint256) +;; c -> (param-id param-cell maybe-hash) +(int, cell, int) parse_config_proposal(cell c) inline_ref { + var cs = c.begin_parse(); + throw_unless(44, cs~load_int(8) == 0xf3 - 0x100); + var (id, val, hash) = (cs~load_int(32), cs~load_maybe_ref(), cs~load_int(1)); + if (hash) { + hash = cs~load_uint(256); + } else { + hash = -1; + } + cs.end_parse(); + return (id, val, hash); +} + +(cell, int, cell) accept_proposal(cell cfg_dict, cell proposal, int critical?) inline_ref { + var (param_id, param_val, req_hash) = parse_config_proposal(proposal); + cell cur_val = cfg_dict.idict_get_ref(32, param_id); + int cur_hash = null?(cur_val) ? 0 : cell_hash(cur_val); + if ((cur_hash != req_hash) & (req_hash >= 0)) { + ;; current value has incorrect hash, do not apply changes + return (cfg_dict, 0, null()); + } + cell mparams = cfg_dict.idict_get_ref(32, 9); ;; mandatory parameters + var (_, found?) = mparams.idict_get?(32, param_id); + if (found? & param_val.null?()) { + ;; cannot set a mandatory parameter to (null) + return (cfg_dict, 0, null()); + } + cell cparams = cfg_dict.idict_get_ref(32, 10); ;; critical parameters + (_, found?) = cparams.idict_get?(32, param_id); + if (found? < critical?) { + ;; trying to set a critical parameter after a non-critical voting + return (cfg_dict, 0, null()); + } + ;; CHANGE ONE CONFIGURATION PARAMETER (!) + cfg_dict~idict_set_ref(32, param_id, param_val); + return (cfg_dict, param_id, param_val); +} + +(cell, int) perform_proposed_action(cell cfg_dict, int public_key, int param_id, cell param_val) inline_ref { + if (param_id == -999) { + ;; appoint or depose dictator + return (cfg_dict, param_val.null?() ? 0 : param_val.begin_parse().preload_uint(256)); + } + if (param_val.null?()) { + return (cfg_dict, public_key); + } + if (param_id == -1000) { + ;; upgrade code + var cs = param_val.begin_parse(); + var new_code = cs~load_ref(); + set_code(new_code); + var old_code = get_c3(); + set_c3(new_code.begin_parse().bless()); + after_code_upgrade(cs, old_code); + throw(0); + return (cfg_dict, public_key); + } + if (param_id == -1001) { + ;; update elector code + var cs = param_val.begin_parse(); + change_elector_code(cs); + } + return (cfg_dict, public_key); +} + +;; cfg_proposal_status#ce expires:uint32 proposal:^ConfigProposal is_critical:Bool +;; voters:(HashmapE 16 True) remaining_weight:int64 validator_set_id:uint256 +;; rounds_remaining:uint8 wins:uint8 losses:uint8 = ConfigProposalStatus; +(int, cell, int, cell, int, int, slice) unpack_proposal_status(slice cs) inline_ref { + throw_unless(44, cs~load_int(8) == 0xce - 0x100); + return (cs~load_uint(32), cs~load_ref(), cs~load_int(1), cs~load_dict(), cs~load_int(64), cs~load_uint(256), cs); +} + +slice update_proposal_status(slice rest, int weight_remaining, int critical?) inline_ref { + var (min_tot_rounds, max_tot_rounds, min_wins, max_losses, _, _, _, _) = get_vote_config(critical?); + var (rounds_remaining, wins, losses) = (rest~load_uint(8), rest~load_uint(8), rest~load_uint(8)); + losses -= (weight_remaining >= 0); + if (losses > max_losses) { + ;; lost too many times + return null(); + } + rounds_remaining -= 1; + if (rounds_remaining < 0) { + ;; existed for too many rounds + return null(); + } + return begin_cell() + .store_uint(rounds_remaining, 8) + .store_uint(wins, 8) + .store_uint(losses, 8) + .end_cell().begin_parse(); +} + +builder begin_pack_proposal_status(int expires, cell proposal, int critical?, cell voters, int weight_remaining, int vset_id) inline { + return begin_cell() + .store_int(0xce - 0x100, 8) + .store_uint(expires, 32) + .store_ref(proposal) + .store_int(critical?, 1) + .store_dict(voters) + .store_int(weight_remaining, 64) + .store_uint(vset_id, 256); +} + +(cell, cell, int) register_vote(vote_dict, phash, idx, weight) inline_ref { + var (pstatus, found?) = vote_dict.udict_get?(256, phash); + ifnot (found?) { + ;; config proposal not found + return (vote_dict, null(), -1); + } + var (cur_vset, total_weight, _) = get_current_vset(); + int cur_vset_id = cur_vset.cell_hash(); + var (expires, proposal, critical?, voters, weight_remaining, vset_id, rest) = unpack_proposal_status(pstatus); + if (expires <= now()) { + ;; config proposal expired, delete and report not found + vote_dict~udict_delete?(256, phash); + return (vote_dict, null(), -1); + } + if (vset_id != cur_vset_id) { + ;; config proposal belongs to a previous validator set + vset_id = cur_vset_id; + rest = update_proposal_status(rest, weight_remaining, critical?); + voters = null(); + weight_remaining = muldiv(total_weight, 3, 4); + } + if (rest.null?()) { + ;; discard proposal (existed for too many rounds, or too many losses) + vote_dict~udict_delete?(256, phash); + return (vote_dict, null(), -1); + } + var (_, found?) = voters.udict_get?(16, idx); + if (found?) { + ;; already voted for this proposal, ignore vote + return (vote_dict, null(), -2); + } + ;; register vote + voters~udict_set_builder(16, idx, begin_cell().store_uint(now(), 32)); + int old_wr = weight_remaining; + weight_remaining -= weight; + if ((weight_remaining ^ old_wr) >= 0) { + ;; not enough votes, or proposal already accepted in this round + ;; simply update weight_remaining + vote_dict~udict_set_builder(256, phash, begin_pack_proposal_status(expires, proposal, critical?, voters, weight_remaining, vset_id).store_slice(rest)); + return (vote_dict, null(), 2); + } + ;; proposal wins in this round + var (min_tot_rounds, max_tot_rounds, min_wins, max_losses, _, _, _, _) = get_vote_config(critical?); + var (rounds_remaining, wins, losses) = (rest~load_uint(8), rest~load_uint(8), rest~load_uint(8)); + wins += 1; + if (wins >= min_wins) { + ;; proposal is accepted, remove and process + vote_dict~udict_delete?(256, phash); + return (vote_dict, proposal, 6 - critical?); + } + ;; update proposal info + vote_dict~udict_set_builder(256, phash, + begin_pack_proposal_status(expires, proposal, critical?, voters, weight_remaining, vset_id) + .store_uint(rounds_remaining, 8) + .store_uint(wins, 8) + .store_uint(losses, 8)); + return (vote_dict, null(), 2); +} + +int proceed_register_vote(phash, idx, weight) impure inline_ref { + var (cfg_dict, stored_seqno, public_key, vote_dict) = load_data(); + (vote_dict, var accepted_proposal, var status) = register_vote(vote_dict, phash, idx, weight); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + ifnot (accepted_proposal.null?()) { + var critical? = 6 - status; + (cfg_dict, var param_id, var param_val) = accept_proposal(cfg_dict, accepted_proposal, critical?); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + if (param_id) { + commit(); + (cfg_dict, public_key) = perform_proposed_action(cfg_dict, public_key, param_id, param_val); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + } + } + return status; +} + +(slice, int) scan_proposal(int phash, slice pstatus) inline_ref { + var (cur_vset, total_weight, _) = get_current_vset(); + int cur_vset_id = cur_vset.cell_hash(); + var (expires, proposal, critical?, voters, weight_remaining, vset_id, rest) = unpack_proposal_status(pstatus); + if (expires <= now()) { + ;; config proposal expired, delete + return (null(), true); + } + if (vset_id == cur_vset_id) { + ;; config proposal already processed or voted for in this round, change nothing + return (pstatus, false); + } + ;; config proposal belongs to a previous validator set + vset_id = cur_vset_id; + rest = update_proposal_status(rest, weight_remaining, critical?); + voters = null(); + weight_remaining = muldiv(total_weight, 3, 4); + if (rest.null?()) { + ;; discard proposal (existed for too many rounds, or too many losses) + return (null(), true); + } + ;; return updated proposal + return (begin_pack_proposal_status(expires, proposal, critical?, voters, weight_remaining, vset_id).store_slice(rest).end_cell().begin_parse(), true); +} + +cell scan_random_proposal(cell vote_dict) inline_ref { + var (phash, pstatus, found?) = vote_dict.udict_get_nexteq?(256, random()); + ifnot (found?) { + return vote_dict; + } + (pstatus, var changed?) = scan_proposal(phash, pstatus); + if (changed?) { + if (pstatus.null?()) { + vote_dict~udict_delete?(256, phash); + } else { + vote_dict~udict_set(256, phash, pstatus); + } + } + return vote_dict; +} + +int register_voting_proposal(slice cs, int msg_value) impure inline_ref { + var (expire_at, proposal, critical?) = (cs~load_uint(32), cs~load_ref(), cs~load_int(1)); + if (expire_at >> 30) { + expire_at -= now(); + } + var (param_id, param_val, hash) = parse_config_proposal(proposal); + if (hash >= 0) { + cell cur_val = config_param(param_id); + int cur_hash = null?(cur_val) ? 0 : cell_hash(cur_val); + if (cur_hash != hash) { + hash = -0xe2646356; ;; bad current value + } + } else { + var m_params = config_param(9); + var (_, found?) = m_params.idict_get?(32, param_id); + if (found?) { + hash = -0xcd506e6c; ;; cannot set mandatory parameter to null + } + } + if (param_val.cell_depth() >= 128) { + hash = -0xc2616456; ;; bad value + } + if (hash < -1) { + return hash; ;; return error if any + } + ifnot (critical?) { + var crit_params = config_param(10); + var (_, found?) = crit_params.idict_get?(32, param_id); + if (found?) { + hash = -0xc3726954; ;; trying to set a critical parameter without critical flag + } + } + if (hash < -1) { + return hash; + } + ;; obtain vote proposal configuration + var vote_cfg = get_vote_config(critical?); + var (min_tot_rounds, max_tot_rounds, min_wins, max_losses, min_store_sec, max_store_sec, bit_price, cell_price) = vote_cfg; + if (expire_at < min_store_sec) { + return -0xc5787069; ;; expired + } + expire_at = min(expire_at, max_store_sec); + ;; compute price + var (_, bits, refs) = compute_data_size(param_val, 1024); + var pps = bit_price * (bits + 1024) + cell_price * (refs + 2); + var price = pps * expire_at; + expire_at += now(); + var (cfg_dict, stored_seqno, public_key, vote_dict) = load_data(); + int phash = proposal.cell_hash(); + var (pstatus, found?) = vote_dict.udict_get?(256, phash); + if (found?) { + ;; proposal already exists; we can only extend it + var (expires, r_proposal, r_critical?, voters, weight_remaining, vset_id, rest) = unpack_proposal_status(pstatus); + if (r_critical? != critical?) { + return -0xc3726955; ;; cannot upgrade critical parameter to non-critical... + } + if (expires >= expire_at) { + return -0xc16c7245; ;; proposal already exists + } + ;; recompute price + price = pps * (expire_at - expires + 16384); + if (msg_value - price < (1 << 30)) { + return -0xf0617924; ;; need more money + } + ;; update expiration time + vote_dict~udict_set_builder(256, phash, begin_pack_proposal_status(expire_at, r_proposal, r_critical?, voters, weight_remaining, vset_id).store_slice(rest)); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + return price; + } + if (msg_value - price < (1 << 30)) { + return -0xf0617924; ;; need more money + } + ;; obtain current validator set data + var (vset, total_weight, _) = get_current_vset(); + int weight_remaining = muldiv(total_weight, 3, 4); + ;; create new proposal + vote_dict~udict_set_builder(256, phash, + begin_pack_proposal_status(expire_at, proposal, critical?, null(), weight_remaining, vset.cell_hash()) + .store_uint(max_tot_rounds, 8).store_uint(0, 16)); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + return price; +} + +() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure { + var cs = in_msg_cell.begin_parse(); + var flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + var s_addr = cs~load_msg_addr(); + (int src_wc, int src_addr) = s_addr.parse_std_addr(); + if ((src_wc + 1) | (flags & 1) | in_msg.slice_empty?()) { + ;; source not in masterchain, or a bounced message, or a simple transfer + return (); + } + int tag = in_msg~load_uint(32); + int query_id = in_msg~load_uint(64); + if (tag == 0x4e565354) { + ;; set next validator set + var vset = in_msg~load_ref(); + in_msg.end_parse(); + var elector_param = config_param(1); + var elector_addr = cell_null?(elector_param) ? -1 : elector_param.begin_parse().preload_uint(256); + var ok = false; + if (src_addr == elector_addr) { + ;; message from elector smart contract + ;; set next validator set + (var t_since, var t_until) = check_validator_set(vset); + var t = now(); + ok = (t_since > t) & (t_until > t_since); + } + if (ok) { + set_conf_param(36, vset); + ;; send confirmation + return send_confirmation(s_addr, query_id, 0xee764f4b); + } else { + return send_error(s_addr, query_id, 0xee764f6f); + } + } + if (tag == 0x6e565052) { + ;; new voting proposal + var price = register_voting_proposal(in_msg, msg_value); + int mode = 64; + int ans_tag = - price; + if (price >= 0) { + ;; ok, debit price + raw_reserve(price, 4); + ans_tag = 0xee565052; + mode = 128; + } + return send_answer(s_addr, query_id, ans_tag, mode); + } + if (tag == 0x566f7465) { + ;; vote for a configuration proposal + var signature = in_msg~load_bits(512); + var msg_body = in_msg; + var (sign_tag, idx, phash) = (in_msg~load_uint(32), in_msg~load_uint(16), in_msg~load_uint(256)); + in_msg.end_parse(); + throw_unless(37, sign_tag == 0x566f7445); + var (vdescr, total_weight) = get_validator_descr(idx); + var (val_pubkey, weight) = unpack_validator_descr(vdescr); + throw_unless(34, check_data_signature(msg_body, signature, val_pubkey)); + int res = proceed_register_vote(phash, idx, weight); + return send_confirmation(s_addr, query_id, res + 0xd6745240); + } + ;; if tag is non-zero and its higher bit is zero, throw an exception (the message is an unsupported query) + ;; to bounce message back to sender + throw_unless(37, (tag == 0) | (tag & (1 << 31))); + ;; do nothing for other internal messages +} + +() recv_external(slice in_msg) impure { + var signature = in_msg~load_bits(512); + var cs = in_msg; + int action = cs~load_uint(32); + int msg_seqno = cs~load_uint(32); + var valid_until = cs~load_uint(32); + throw_if(35, valid_until < now()); + throw_if(39, slice_depth(cs) > 128); + var (cfg_dict, stored_seqno, public_key, vote_dict) = load_data(); + throw_unless(33, msg_seqno == stored_seqno); + if (action == 0x566f7465) { + ;; vote for a configuration proposal + var (idx, phash) = (cs~load_uint(16), cs~load_uint(256)); + cs.end_parse(); + var (vdescr, total_weight) = get_validator_descr(idx); + var (val_pubkey, weight) = unpack_validator_descr(vdescr); + throw_unless(34, check_data_signature(in_msg, signature, val_pubkey)); + accept_message(); + stored_seqno = (stored_seqno + 1) % (1 << 32); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + commit(); + proceed_register_vote(phash, idx, weight); + return (); + } + throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key)); + accept_message(); + stored_seqno = (stored_seqno + 1) % (1 << 32); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); + commit(); + (cfg_dict, public_key) = perform_action(cfg_dict, public_key, action, cs); + store_data(cfg_dict, stored_seqno, public_key, vote_dict); +} + +() run_ticktock(int is_tock) impure { + var (cfg_dict, stored_seqno, public_key, vote_dict) = load_data(); + int kl = 32; + var next_vset = cfg_dict.idict_get_ref(kl, 36); + var updated? = false; + ifnot (next_vset.null?()) { + ;; check whether we have to set next_vset as the current validator set + var ds = next_vset.begin_parse(); + if (ds.slice_bits() >= 40) { + var tag = ds~load_uint(8); + var since = ds.preload_uint(32); + if ((since <= now()) & (tag == 0x12)) { + ;; next validator set becomes active! + var cur_vset = cfg_dict~idict_set_get_ref(kl, 34, next_vset); ;; next_vset -> cur_vset + cfg_dict~idict_set_get_ref(kl, 32, cur_vset); ;; cur_vset -> prev_vset + cfg_dict~idict_delete?(kl, 36); ;; (null) -> next_vset + updated? = true; + } + } + } + ifnot (updated?) { + ;; if nothing has been done so far, scan a random voting proposal instead + vote_dict = scan_random_proposal(vote_dict); + } + ;; save data and return + return store_data(cfg_dict, stored_seqno, public_key, vote_dict); +} + +int seqno() method_id { + return get_data().begin_parse().preload_uint(32); +} + +_ unpack_proposal(slice pstatus) inline_ref { + (int expires, cell proposal, int critical?, cell voters, int weight_remaining, int vset_id, slice rest) = unpack_proposal_status(pstatus); + var voters_list = null(); + var voter_id = (1 << 32); + do { + (voter_id, _, var f) = voters.udict_get_prev?(16, voter_id); + if (f) { + voters_list = cons(voter_id, voters_list); + } + } until (~ f); + ;; Note there is a bug in config contract currently deployed in testnet2: + ;; wins and losses are messed up + var (rounds_remaining, wins, losses) = (rest~load_uint(8), rest~load_uint(8), rest~load_uint(8)); + rest.end_parse(); + var (param_id, param_val, param_hash) = parse_config_proposal(proposal); + return [expires, critical?, [param_id, param_val, param_hash], vset_id, voters_list, weight_remaining, rounds_remaining, losses, wins]; +} + +_ get_proposal(int phash) method_id { + (_, _, _, var vote_dict) = load_data(); + var (pstatus, found?) = vote_dict.udict_get?(256, phash); + ifnot (found?) { + return null(); + } + return unpack_proposal(pstatus); +} + +_ list_proposals() method_id { + (_, _, _, var vote_dict) = load_data(); + var phash = (1 << 255) + ((1 << 255) - 1); + var list = null(); + do { + (phash, var pstatus, var f) = vote_dict.udict_get_prev?(256, phash); + if (f) { + list = cons([phash, unpack_proposal(pstatus)], list); + } + } until (~ f); + return list; +} + +_ proposal_storage_price(int critical?, int seconds, int bits, int refs) method_id { + var cfg_dict = get_data().begin_parse().preload_ref(); + var cparam11 = cfg_dict.idict_get_ref(32, 11); + var (min_tot_rounds, max_tot_rounds, min_wins, max_losses, min_store_sec, max_store_sec, bit_price, cell_price) = get_vote_config_internal(critical?, cparam11); + if (seconds < min_store_sec) { + return -1; + } + seconds = min(seconds, max_store_sec); + return (bit_price * (bits + 1024) + cell_price * (refs + 2)) * seconds; +} diff --git a/elector/elector-code.fc b/elector/elector-code.fc new file mode 100644 index 0000000..97185fc --- /dev/null +++ b/elector/elector-code.fc @@ -0,0 +1,1187 @@ +;; Elector smartcontract + +;; cur_elect credits past_elections grams active_id active_hash +(cell, cell, cell, int, int, int) load_data() inline_ref { + var cs = get_data().begin_parse(); + var res = (cs~load_dict(), cs~load_dict(), cs~load_dict(), cs~load_grams(), cs~load_uint(32), cs~load_uint(256)); + cs.end_parse(); + return res; +} + +;; cur_elect credits past_elections grams active_id active_hash +() store_data(elect, credits, past_elections, grams, active_id, active_hash) impure inline_ref { + set_data(begin_cell() + .store_dict(elect) + .store_dict(credits) + .store_dict(past_elections) + .store_grams(grams) + .store_uint(active_id, 32) + .store_uint(active_hash, 256) + .end_cell()); +} + +;; elect -> elect_at elect_close min_stake total_stake members failed finished +_ unpack_elect(elect) inline_ref { + var es = elect.begin_parse(); + var res = (es~load_uint(32), es~load_uint(32), es~load_grams(), es~load_grams(), es~load_dict(), es~load_int(1), es~load_int(1)); + es.end_parse(); + return res; +} + +cell pack_elect(elect_at, elect_close, min_stake, total_stake, members, failed, finished) inline_ref { + return begin_cell() + .store_uint(elect_at, 32) + .store_uint(elect_close, 32) + .store_grams(min_stake) + .store_grams(total_stake) + .store_dict(members) + .store_int(failed, 1) + .store_int(finished, 1) + .end_cell(); +} + +;; slice -> unfreeze_at stake_held vset_hash frozen_dict total_stake bonuses complaints +_ unpack_past_election(slice fs) inline_ref { + var res = (fs~load_uint(32), fs~load_uint(32), fs~load_uint(256), fs~load_dict(), fs~load_grams(), fs~load_grams(), fs~load_dict()); + fs.end_parse(); + return res; +} + +builder pack_past_election(int unfreeze_at, int stake_held, int vset_hash, cell frozen_dict, int total_stake, int bonuses, cell complaints) inline_ref { + return begin_cell() + .store_uint(unfreeze_at, 32) + .store_uint(stake_held, 32) + .store_uint(vset_hash, 256) + .store_dict(frozen_dict) + .store_grams(total_stake) + .store_grams(bonuses) + .store_dict(complaints); +} + +;; complaint_status#2d complaint:^ValidatorComplaint voters:(HashmapE 16 True) +;; vset_id:uint256 weight_remaining:int64 = ValidatorComplaintStatus; +_ unpack_complaint_status(slice cs) inline_ref { + throw_unless(9, cs~load_uint(8) == 0x2d); + var res = (cs~load_ref(), cs~load_dict(), cs~load_uint(256), cs~load_int(64)); + cs.end_parse(); + return res; +} + +builder pack_complaint_status(cell complaint, cell voters, int vset_id, int weight_remaining) inline_ref { + return begin_cell() + .store_uint(0x2d, 8) + .store_ref(complaint) + .store_dict(voters) + .store_uint(vset_id, 256) + .store_int(weight_remaining, 64); +} + +;; validator_complaint#bc validator_pubkey:uint256 description:^ComplaintDescr +;; created_at:uint32 severity:uint8 reward_addr:uint256 paid:Grams suggested_fine:Grams +;; suggested_fine_part:uint32 = ValidatorComplaint; +_ unpack_complaint(slice cs) inline_ref { + throw_unless(9, cs~load_int(8) == 0xbc - 0x100); + var res = (cs~load_uint(256), cs~load_ref(), cs~load_uint(32), cs~load_uint(8), cs~load_uint(256), cs~load_grams(), cs~load_grams(), cs~load_uint(32)); + cs.end_parse(); + return res; +} + +builder pack_complaint(int validator_pubkey, cell description, int created_at, int severity, int reward_addr, int paid, int suggested_fine, int suggested_fine_part) inline_ref { + return begin_cell() + .store_int(0xbc - 0x100, 8) + .store_uint(validator_pubkey, 256) + .store_ref(description) + .store_uint(created_at, 32) + .store_uint(severity, 8) + .store_uint(reward_addr, 256) + .store_grams(paid) + .store_grams(suggested_fine) + .store_uint(suggested_fine_part, 32); +} + +;; complaint_prices#1a deposit:Grams bit_price:Grams cell_price:Grams = ComplaintPricing; +(int, int, int) parse_complaint_prices(cell info) inline { + var cs = info.begin_parse(); + throw_unless(9, cs~load_uint(8) == 0x1a); + var res = (cs~load_grams(), cs~load_grams(), cs~load_grams()); + cs.end_parse(); + return res; +} + +;; deposit bit_price cell_price +(int, int, int) get_complaint_prices() inline_ref { + var info = config_param(13); + return info.null?() ? (1 << 36, 1, 512) : info.parse_complaint_prices(); +} + +;; elected_for elections_begin_before elections_end_before stake_held_for +(int, int, int, int) get_validator_conf() { + var cs = config_param(15).begin_parse(); + return (cs~load_int(32), cs~load_int(32), cs~load_int(32), cs.preload_int(32)); +} + +;; next three functions return information about current validator set (config param #34) +;; they are borrowed from config-code.fc +(cell, int, cell) get_current_vset() inline_ref { + var vset = config_param(34); + var cs = begin_parse(vset); + ;; validators_ext#12 utime_since:uint32 utime_until:uint32 + ;; total:(## 16) main:(## 16) { main <= total } { main >= 1 } + ;; total_weight:uint64 + throw_unless(40, cs~load_uint(8) == 0x12); + cs~skip_bits(32 + 32 + 16 + 16); + var (total_weight, dict) = (cs~load_uint(64), cs~load_dict()); + cs.end_parse(); + return (vset, total_weight, dict); +} + +(slice, int) get_validator_descr(int idx) inline_ref { + var (vset, total_weight, dict) = get_current_vset(); + var (value, _) = dict.udict_get?(16, idx); + return (value, total_weight); +} + +(int, int) unpack_validator_descr(slice cs) inline { + ;; ed25519_pubkey#8e81278a pubkey:bits256 = SigPubKey; + ;; validator#53 public_key:SigPubKey weight:uint64 = ValidatorDescr; + ;; validator_addr#73 public_key:SigPubKey weight:uint64 adnl_addr:bits256 = ValidatorDescr; + throw_unless(41, (cs~load_uint(8) & ~ 0x20) == 0x53); + throw_unless(41, cs~load_uint(32) == 0x8e81278a); + return (cs~load_uint(256), cs~load_uint(64)); +} + +() send_message_back(addr, ans_tag, query_id, body, grams, mode) impure inline_ref { + ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 + var msg = begin_cell() + .store_uint(0x18, 6) + .store_slice(addr) + .store_grams(grams) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(ans_tag, 32) + .store_uint(query_id, 64); + if (body >= 0) { + msg~store_uint(body, 32); + } + send_raw_message(msg.end_cell(), mode); +} + +() return_stake(addr, query_id, reason) impure inline_ref { + return send_message_back(addr, 0xee6f454c, query_id, reason, 0, 64); +} + +() send_confirmation(addr, query_id, comment) impure inline_ref { + return send_message_back(addr, 0xf374484c, query_id, comment, 1000000000, 2); +} + +() send_validator_set_to_config(config_addr, vset, query_id) impure inline_ref { + var msg = begin_cell() + .store_uint(0xc4ff, 17) ;; 0 11000100 0xff + .store_uint(config_addr, 256) + .store_grams(1 << 30) ;; ~1 gram of value to process and obtain answer + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(0x4e565354, 32) + .store_uint(query_id, 64) + .store_ref(vset); + send_raw_message(msg.end_cell(), 1); +} + +;; credits 'amount' to 'addr' inside credit dictionary 'credits' +_ ~credit_to(credits, addr, amount) inline_ref { + var (val, f) = credits.udict_get?(256, addr); + if (f) { + amount += val~load_grams(); + } + credits~udict_set_builder(256, addr, begin_cell().store_grams(amount)); + return (credits, ()); +} + +() process_new_stake(s_addr, msg_value, cs, query_id) impure inline_ref { + var (src_wc, src_addr) = parse_std_addr(s_addr); + var ds = get_data().begin_parse(); + var elect = ds~load_dict(); + if (elect.null?() | (src_wc + 1)) { + ;; no elections active, or source is not in masterchain + ;; bounce message + return return_stake(s_addr, query_id, 0); + } + ;; parse the remainder of new stake message + var validator_pubkey = cs~load_uint(256); + var stake_at = cs~load_uint(32); + var max_factor = cs~load_uint(32); + var adnl_addr = cs~load_uint(256); + var signature = cs~load_ref().begin_parse().preload_bits(512); + cs.end_parse(); + ifnot (check_data_signature(begin_cell() + .store_uint(0x654c5074, 32) + .store_uint(stake_at, 32) + .store_uint(max_factor, 32) + .store_uint(src_addr, 256) + .store_uint(adnl_addr, 256) + .end_cell().begin_parse(), signature, validator_pubkey)) { + ;; incorrect signature, return stake + return return_stake(s_addr, query_id, 1); + } + if (max_factor < 0x10000) { + ;; factor must be >= 1. = 65536/65536 + return return_stake(s_addr, query_id, 6); + } + ;; parse current election data + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + ;; elect_at~dump(); + msg_value -= 1000000000; ;; deduct GR$1 for sending confirmation + if ((msg_value << 12) < total_stake) { + ;; stake smaller than 1/4096 of the total accumulated stakes, return + return return_stake(s_addr, query_id, 2); + } + total_stake += msg_value; ;; (provisionally) increase total stake + if (stake_at != elect_at) { + ;; stake for some other elections, return + return return_stake(s_addr, query_id, 3); + } + if (finished) { + ;; elections already finished, return stake + return return_stake(s_addr, query_id, 0); + } + var (mem, found) = members.udict_get?(256, validator_pubkey); + if (found) { + ;; entry found, merge stakes + msg_value += mem~load_grams(); + mem~load_uint(64); ;; skip timestamp and max_factor + found = (src_addr != mem~load_uint(256)); + } + if (found) { + ;; can make stakes for a public key from one address only + return return_stake(s_addr, query_id, 4); + } + if (msg_value < min_stake) { + ;; stake too small, return it + return return_stake(s_addr, query_id, 5); + } + throw_unless(44, msg_value); + accept_message(); + ;; store stake in the dictionary + members~udict_set_builder(256, validator_pubkey, begin_cell() + .store_grams(msg_value) + .store_uint(now(), 32) + .store_uint(max_factor, 32) + .store_uint(src_addr, 256) + .store_uint(adnl_addr, 256)); + ;; gather and save election data + elect = pack_elect(elect_at, elect_close, min_stake, total_stake, members, false, false); + set_data(begin_cell().store_dict(elect).store_slice(ds).end_cell()); + ;; return confirmation message + if (query_id) { + return send_confirmation(s_addr, query_id, 0); + } + return (); +} + +(cell, int) unfreeze_without_bonuses(credits, freeze_dict, tot_stakes) inline_ref { + var total = var recovered = 0; + var pubkey = -1; + do { + (pubkey, var cs, var f) = freeze_dict.udict_get_next?(256, pubkey); + if (f) { + var (addr, weight, stake, banned) = (cs~load_uint(256), cs~load_uint(64), cs~load_grams(), cs~load_int(1)); + cs.end_parse(); + if (banned) { + recovered += stake; + } else { + credits~credit_to(addr, stake); + } + total += stake; + } + } until (~ f); + throw_unless(59, total == tot_stakes); + return (credits, recovered); +} + +(cell, int) unfreeze_with_bonuses(credits, freeze_dict, tot_stakes, tot_bonuses) inline_ref { + var total = var recovered = var returned_bonuses = 0; + var pubkey = -1; + do { + (pubkey, var cs, var f) = freeze_dict.udict_get_next?(256, pubkey); + if (f) { + var (addr, weight, stake, banned) = (cs~load_uint(256), cs~load_uint(64), cs~load_grams(), cs~load_int(1)); + cs.end_parse(); + if (banned) { + recovered += stake; + } else { + var bonus = muldiv(tot_bonuses, stake, tot_stakes); + returned_bonuses += bonus; + credits~credit_to(addr, stake + bonus); + } + total += stake; + } + } until (~ f); + throw_unless(59, (total == tot_stakes) & (returned_bonuses <= tot_bonuses)); + return (credits, recovered + tot_bonuses - returned_bonuses); +} + +int stakes_sum(frozen_dict) inline_ref { + var total = 0; + var pubkey = -1; + do { + (pubkey, var cs, var f) = frozen_dict.udict_get_next?(256, pubkey); + if (f) { + cs~skip_bits(256 + 64); + total += cs~load_grams(); + } + } until (~ f); + return total; +} + +_ unfreeze_all(credits, past_elections, elect_id) inline_ref { + var (fs, f) = past_elections~udict_delete_get?(32, elect_id); + ifnot (f) { + ;; no elections with this id + return (credits, past_elections, 0); + } + var (unfreeze_at, stake_held, vset_hash, fdict, tot_stakes, bonuses, complaints) = fs.unpack_past_election(); + ;; tot_stakes = fdict.stakes_sum(); ;; TEMP BUGFIX + var unused_prizes = (bonuses > 0) ? + credits~unfreeze_with_bonuses(fdict, tot_stakes, bonuses) : + credits~unfreeze_without_bonuses(fdict, tot_stakes); + return (credits, past_elections, unused_prizes); +} + +() config_set_confirmed(s_addr, cs, query_id, ok) impure inline_ref { + var (src_wc, src_addr) = parse_std_addr(s_addr); + var config_addr = config_param(0).begin_parse().preload_uint(256); + var ds = get_data().begin_parse(); + var elect = ds~load_dict(); + if ((src_wc + 1) | (src_addr != config_addr) | elect.null?()) { + ;; not from config smc, somebody's joke? + ;; or no elections active (or just completed) + return (); + } + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + if ((elect_at != query_id) | ~ finished) { + ;; not these elections, or elections not finished yet + return (); + } + accept_message(); + ifnot (ok) { + ;; cancel elections, return stakes + var (credits, past_elections, grams) = (ds~load_dict(), ds~load_dict(), ds~load_grams()); + (credits, past_elections, var unused_prizes) = unfreeze_all(credits, past_elections, elect_at); + set_data(begin_cell() + .store_int(false, 1) + .store_dict(credits) + .store_dict(past_elections) + .store_grams(grams + unused_prizes) + .store_slice(ds) + .end_cell()); + } + ;; ... do not remove elect until we see this set as the next elected validator set +} + +() process_simple_transfer(s_addr, msg_value) impure inline_ref { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + (int src_wc, int src_addr) = parse_std_addr(s_addr); + if (src_addr | (src_wc + 1) | (active_id == 0)) { + ;; simple transfer to us (credit "nobody's" account) + ;; (or no known active validator set) + grams += msg_value; + return store_data(elect, credits, past_elections, grams, active_id, active_hash); + } + ;; zero source address -1:00..00 (collecting validator fees) + var (fs, f) = past_elections.udict_get?(32, active_id); + ifnot (f) { + ;; active validator set not found (?) + grams += msg_value; + } else { + ;; credit active validator set bonuses + var (unfreeze_at, stake_held, hash, dict, total_stake, bonuses, complaints) = fs.unpack_past_election(); + bonuses += msg_value; + past_elections~udict_set_builder(32, active_id, + pack_past_election(unfreeze_at, stake_held, hash, dict, total_stake, bonuses, complaints)); + } + return store_data(elect, credits, past_elections, grams, active_id, active_hash); +} + +() recover_stake(op, s_addr, cs, query_id) impure inline_ref { + (int src_wc, int src_addr) = parse_std_addr(s_addr); + if (src_wc + 1) { + ;; not from masterchain, return error + return send_message_back(s_addr, 0xfffffffe, query_id, op, 0, 64); + } + var ds = get_data().begin_parse(); + var (elect, credits) = (ds~load_dict(), ds~load_dict()); + var (cs, f) = credits~udict_delete_get?(256, src_addr); + ifnot (f) { + ;; no credit for sender, return error + return send_message_back(s_addr, 0xfffffffe, query_id, op, 0, 64); + } + var amount = cs~load_grams(); + cs.end_parse(); + ;; save data + set_data(begin_cell().store_dict(elect).store_dict(credits).store_slice(ds).end_cell()); + ;; send amount to sender in a new message + send_raw_message(begin_cell() + .store_uint(0x18, 6) + .store_slice(s_addr) + .store_grams(amount) + .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) + .store_uint(0xf96f7324, 32) + .store_uint(query_id, 64) + .end_cell(), 64); +} + +() after_code_upgrade(slice s_addr, slice cs, int query_id) impure method_id(1666) { + var op = 0x4e436f64; + return send_message_back(s_addr, 0xce436f64, query_id, op, 0, 64); +} + +int upgrade_code(s_addr, cs, query_id) inline_ref { + var c_addr = config_param(0); + if (c_addr.null?()) { + ;; no configuration smart contract known + return false; + } + var config_addr = c_addr.begin_parse().preload_uint(256); + var (src_wc, src_addr) = parse_std_addr(s_addr); + if ((src_wc + 1) | (src_addr != config_addr)) { + ;; not from configuration smart contract, return error + return false; + } + accept_message(); + var code = cs~load_ref(); + set_code(code); + ifnot(cs.slice_empty?()) { + set_c3(code.begin_parse().bless()); + after_code_upgrade(s_addr, cs, query_id); + throw(0); + } + return true; +} + +int register_complaint(s_addr, complaint, msg_value) { + var (src_wc, src_addr) = parse_std_addr(s_addr); + if (src_wc + 1) { ;; not from masterchain, return error + return -1; + } + if (complaint.slice_depth() >= 128) { + return -3; ;; invalid complaint + } + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var election_id = complaint~load_uint(32); + var (fs, f) = past_elections.udict_get?(32, election_id); + ifnot (f) { ;; election not found + return -2; + } + var expire_in = fs.preload_uint(32) - now(); + if (expire_in <= 0) { ;; already expired + return -4; + } + var (validator_pubkey, description, created_at, severity, reward_addr, paid, suggested_fine, suggested_fine_part) = unpack_complaint(complaint); + reward_addr = src_addr; + created_at = now(); + ;; compute complaint storage/creation price + var (deposit, bit_price, cell_price) = get_complaint_prices(); + var (_, bits, refs) = slice_compute_data_size(complaint, 4096); + var pps = (bits + 1024) * bit_price + (refs + 2) * cell_price; + paid = pps * expire_in + deposit; + if (msg_value < paid + (1 << 30)) { ;; not enough money + return -5; + } + ;; re-pack modified complaint + cell complaint = pack_complaint(validator_pubkey, description, created_at, severity, reward_addr, paid, suggested_fine, suggested_fine_part).end_cell(); + var (unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints) = unpack_past_election(fs); + var (fs, f) = frozen_dict.udict_get?(256, validator_pubkey); + ifnot (f) { ;; no such validator, cannot complain + return -6; + } + fs~skip_bits(256 + 64); ;; addr weight + var validator_stake = fs~load_grams(); + int fine = suggested_fine + muldiv(validator_stake, suggested_fine_part, 1 << 32); + if (fine > validator_stake) { ;; validator's stake is less than suggested fine + return -7; + } + if (fine <= paid) { ;; fine is less than the money paid for creating complaint + return -8; + } + ;; create complaint status + var cstatus = pack_complaint_status(complaint, null(), 0, 0); + ;; save complaint status into complaints + var cpl_id = complaint.cell_hash(); + ifnot (complaints~udict_add_builder?(256, cpl_id, cstatus)) { + return -9; ;; complaint already exists + } + ;; pack past election info + past_elections~udict_set_builder(32, election_id, pack_past_election(unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints)); + ;; pack persistent data + ;; next line can be commented, but it saves a lot of stack manipulations + var (elect, credits, _, grams, active_id, active_hash) = load_data(); + store_data(elect, credits, past_elections, grams, active_id, active_hash); + return paid; +} + +(cell, cell, int, int) punish(credits, frozen, complaint) inline_ref { + var (validator_pubkey, description, created_at, severity, reward_addr, paid, suggested_fine, suggested_fine_part) = complaint.begin_parse().unpack_complaint(); + var (cs, f) = frozen.udict_get?(256, validator_pubkey); + ifnot (f) { + ;; no validator to punish + return (credits, frozen, 0, 0); + } + var (addr, weight, stake, banned) = (cs~load_uint(256), cs~load_uint(64), cs~load_grams(), cs~load_int(1)); + cs.end_parse(); + int fine = min(stake, suggested_fine + muldiv(stake, suggested_fine_part, 1 << 32)); + stake -= fine; + frozen~udict_set_builder(256, validator_pubkey, begin_cell() + .store_uint(addr, 256) + .store_uint(weight, 64) + .store_grams(stake) + .store_int(banned, 1)); + int reward = min(fine >> 3, paid * 8); + credits~credit_to(reward_addr, reward); + return (credits, frozen, fine - reward, fine); +} + +(cell, cell, int) register_vote(complaints, chash, idx, weight) inline_ref { + var (cstatus, found?) = complaints.udict_get?(256, chash); + ifnot (found?) { + ;; complaint not found + return (complaints, null(), -1); + } + var (cur_vset, total_weight, _) = get_current_vset(); + int cur_vset_id = cur_vset.cell_hash(); + var (complaint, voters, vset_id, weight_remaining) = unpack_complaint_status(cstatus); + int vset_old? = (vset_id != cur_vset_id); + if ((weight_remaining < 0) & vset_old?) { + ;; previous validator set already collected 2/3 votes, skip new votes + return (complaints, null(), -3); + } + if (vset_old?) { + ;; complaint votes belong to a previous validator set, reset voting + vset_id = cur_vset_id; + voters = null(); + weight_remaining = muldiv(total_weight, 2, 3); + } + var (_, found?) = voters.udict_get?(16, idx); + if (found?) { + ;; already voted for this proposal, ignore vote + return (complaints, null(), 0); + } + ;; register vote + voters~udict_set_builder(16, idx, begin_cell().store_uint(now(), 32)); + int old_wr = weight_remaining; + weight_remaining -= weight; + old_wr ^= weight_remaining; + ;; save voters and weight_remaining + complaints~udict_set_builder(256, chash, pack_complaint_status(complaint, voters, vset_id, weight_remaining)); + if (old_wr >= 0) { + ;; not enough votes or already accepted + return (complaints, null(), 1); + } + ;; complaint wins, prepare punishment + return (complaints, complaint, 2); +} + +int proceed_register_vote(election_id, chash, idx, weight) impure inline_ref { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var (fs, f) = past_elections.udict_get?(32, election_id); + ifnot (f) { ;; election not found + return -2; + } + var (unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints) = unpack_past_election(fs); + (complaints, var accepted_complaint, var status) = register_vote(complaints, chash, idx, weight); + if (status <= 0) { + return status; + } + ifnot (accepted_complaint.null?()) { + (credits, frozen_dict, int fine_unalloc, int fine_collected) = punish(credits, frozen_dict, accepted_complaint); + grams += fine_unalloc; + total_stake -= fine_collected; + } + past_elections~udict_set_builder(32, election_id, pack_past_election(unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints)); + store_data(elect, credits, past_elections, grams, active_id, active_hash); + return status; +} + +() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure { + ;; do nothing for internal messages + var cs = in_msg_cell.begin_parse(); + var flags = cs~load_uint(4); ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool + if (flags & 1) { + ;; ignore all bounced messages + return (); + } + var s_addr = cs~load_msg_addr(); + if (in_msg.slice_empty?()) { + ;; inbound message has empty body + return process_simple_transfer(s_addr, msg_value); + } + int op = in_msg~load_uint(32); + if (op == 0) { + ;; simple transfer with comment, return + return process_simple_transfer(s_addr, msg_value); + } + int query_id = in_msg~load_uint(64); + if (op == 0x4e73744b) { + ;; new stake message + return process_new_stake(s_addr, msg_value, in_msg, query_id); + } + if (op == 0x47657424) { + ;; recover stake request + return recover_stake(op, s_addr, in_msg, query_id); + } + if (op == 0x4e436f64) { + ;; upgrade code (accepted only from configuration smart contract) + var ok = upgrade_code(s_addr, in_msg, query_id); + return send_message_back(s_addr, ok ? 0xce436f64 : 0xffffffff, query_id, op, 0, 64); + } + var cfg_ok = (op == 0xee764f4b); + if (cfg_ok | (op == 0xee764f6f)) { + ;; confirmation from configuration smart contract + return config_set_confirmed(s_addr, in_msg, query_id, cfg_ok); + } + if (op == 0x52674370) { + ;; new complaint + var price = register_complaint(s_addr, in_msg, msg_value); + int mode = 64; + int ans_tag = - price; + if (price >= 0) { + ;; ok, debit price + raw_reserve(price, 4); + ans_tag = 0; + mode = 128; + } + return send_message_back(s_addr, ans_tag + 0xf2676350, query_id, op, 0, mode); + } + if (op == 0x56744370) { + ;; vote for a complaint + var signature = in_msg~load_bits(512); + var msg_body = in_msg; + var (sign_tag, idx, elect_id, chash) = (in_msg~load_uint(32), in_msg~load_uint(16), in_msg~load_uint(32), in_msg~load_uint(256)); + in_msg.end_parse(); + throw_unless(37, sign_tag == 0x56744350); + var (vdescr, total_weight) = get_validator_descr(idx); + var (val_pubkey, weight) = unpack_validator_descr(vdescr); + throw_unless(34, check_data_signature(msg_body, signature, val_pubkey)); + int res = proceed_register_vote(elect_id, chash, idx, weight); + return send_message_back(s_addr, res + 0xd6745240, query_id, op, 0, 64); + } + + ifnot (op & (1 << 31)) { + ;; unknown query, return error + return send_message_back(s_addr, 0xffffffff, query_id, op, 0, 64); + } + ;; unknown answer, ignore + return (); +} + +int postpone_elections() impure { + return false; +} + +;; computes the total stake out of the first n entries of list l +_ compute_total_stake(l, n, m_stake) inline_ref { + int tot_stake = 0; + repeat (n) { + (var h, l) = uncons(l); + var stake = h.at(0); + var max_f = h.at(1); + stake = min(stake, (max_f * m_stake) >> 16); + tot_stake += stake; + } + return tot_stake; +} + +(cell, cell, int, cell, int, int) try_elect(credits, members, min_stake, max_stake, min_total_stake, max_stake_factor) { + var cs = 16.config_param().begin_parse(); + var (max_validators, _, min_validators) = (cs~load_uint(16), cs~load_uint(16), cs~load_uint(16)); + cs.end_parse(); + min_validators = max(min_validators, 1); + int n = 0; + var sdict = new_dict(); + var pubkey = -1; + do { + (pubkey, var cs, var f) = members.udict_get_next?(256, pubkey); + if (f) { + var (stake, time, max_factor, addr, adnl_addr) = (cs~load_grams(), cs~load_uint(32), cs~load_uint(32), cs~load_uint(256), cs~load_uint(256)); + cs.end_parse(); + var key = begin_cell() + .store_uint(stake, 128) + .store_int(- time, 32) + .store_uint(pubkey, 256) + .end_cell().begin_parse(); + sdict~dict_set_builder(128 + 32 + 256, key, begin_cell() + .store_uint(min(max_factor, max_stake_factor), 32) + .store_uint(addr, 256) + .store_uint(adnl_addr, 256)); + n += 1; + } + } until (~ f); + n = min(n, max_validators); + if (n < min_validators) { + return (credits, new_dict(), 0, new_dict(), 0, 0); + } + var l = nil; + do { + var (key, cs, f) = sdict~dict::delete_get_min(128 + 32 + 256); + if (f) { + var (stake, _, pubkey) = (min(key~load_uint(128), max_stake), key~load_uint(32), key.preload_uint(256)); + var (max_f, _, adnl_addr) = (cs~load_uint(32), cs~load_uint(256), cs.preload_uint(256)); + l = cons([stake, max_f, pubkey, adnl_addr], l); + } + } until (~ f); + ;; l is the list of all stakes in decreasing order + int i = min_validators - 1; + var l1 = l; + repeat (i) { + l1 = cdr(l1); + } + var (best_stake, m) = (0, 0); + do { + var stake = l1~list_next().at(0); + i += 1; + if (stake >= min_stake) { + var tot_stake = compute_total_stake(l, i, stake); + if (tot_stake > best_stake) { + (best_stake, m) = (tot_stake, i); + } + } + } until (i >= n); + if ((m == 0) | (best_stake < min_total_stake)) { + return (credits, new_dict(), 0, new_dict(), 0, 0); + } + ;; we have to select first m validators from list l + l1 = touch(l); + ;; l1~dump(); ;; DEBUG + repeat (m - 1) { + l1 = cdr(l1); + } + var m_stake = car(l1).at(0); ;; minimal stake + ;; create both the new validator set and the refund set + int i = 0; + var tot_stake = 0; + var tot_weight = 0; + var vset = new_dict(); + var frozen = new_dict(); + do { + var [stake, max_f, pubkey, adnl_addr] = l~list_next(); + ;; lookup source address first + var (val, f) = members.udict_get?(256, pubkey); + throw_unless(61, f); + (_, _, var src_addr) = (val~load_grams(), val~load_uint(64), val.preload_uint(256)); + if (i < m) { + ;; one of the first m members, include into validator set + var true_stake = min(stake, (max_f * m_stake) >> 16); + stake -= true_stake; + ;; ed25519_pubkey#8e81278a pubkey:bits256 = SigPubKey; // 288 bits + ;; validator_addr#73 public_key:SigPubKey weight:uint64 adnl_addr:bits256 = ValidatorDescr; + var weight = (true_stake << 60) / best_stake; + tot_stake += true_stake; + tot_weight += weight; + var vinfo = begin_cell() + .store_uint(adnl_addr ? 0x73 : 0x53, 8) ;; validator_addr#73 or validator#53 + .store_uint(0x8e81278a, 32) ;; ed25519_pubkey#8e81278a + .store_uint(pubkey, 256) ;; pubkey:bits256 + .store_uint(weight, 64); ;; weight:uint64 + if (adnl_addr) { + vinfo~store_uint(adnl_addr, 256); ;; adnl_addr:bits256 + } + vset~udict_set_builder(16, i, vinfo); + frozen~udict_set_builder(256, pubkey, begin_cell() + .store_uint(src_addr, 256) + .store_uint(weight, 64) + .store_grams(true_stake) + .store_int(false, 1)); + } + if (stake) { + ;; non-zero unused part of the stake, credit to the source address + credits~credit_to(src_addr, stake); + } + i += 1; + } until (l.null?()); + throw_unless(49, tot_stake == best_stake); + return (credits, vset, tot_weight, frozen, tot_stake, m); +} + +int conduct_elections(ds, elect, credits) impure { + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + if (now() < elect_close) { + ;; elections not finished yet + return false; + } + if (config_param(0).null?()) { + ;; no configuration smart contract to send result to + return postpone_elections(); + } + var cs = config_param(17).begin_parse(); + min_stake = cs~load_grams(); + var max_stake = cs~load_grams(); + var min_total_stake = cs~load_grams(); + var max_stake_factor = cs~load_uint(32); + cs.end_parse(); + if (total_stake < min_total_stake) { + ;; insufficient total stake, postpone elections + return postpone_elections(); + } + if (failed) { + ;; do not retry failed elections until new stakes arrive + return postpone_elections(); + } + if (finished) { + ;; elections finished + return false; + } + (credits, var vdict, var total_weight, var frozen, var total_stakes, var cnt) = try_elect(credits, members, min_stake, max_stake, min_total_stake, max_stake_factor); + ;; pack elections; if cnt==0, set failed=true, finished=false. + failed = (cnt == 0); + finished = ~ failed; + elect = pack_elect(elect_at, elect_close, min_stake, total_stake, members, failed, finished); + ifnot (cnt) { + ;; elections failed, set elect_failed to true + set_data(begin_cell().store_dict(elect).store_dict(credits).store_slice(ds).end_cell()); + return postpone_elections(); + } + ;; serialize a query to the configuration smart contract + ;; to install the computed validator set as the next validator set + var (elect_for, elect_begin_before, elect_end_before, stake_held) = get_validator_conf(); + var start = max(now() + elect_end_before - 60, elect_at); + var main_validators = config_param(16).begin_parse().skip_bits(16).preload_uint(16); + var vset = begin_cell() + .store_uint(0x12, 8) ;; validators_ext#12 + .store_uint(start, 32) ;; utime_since:uint32 + .store_uint(start + elect_for, 32) ;; utime_until:uint32 + .store_uint(cnt, 16) ;; total:(## 16) + .store_uint(min(cnt, main_validators), 16) ;; main:(## 16) + .store_uint(total_weight, 64) ;; total_weight:uint64 + .store_dict(vdict) ;; list:(HashmapE 16 ValidatorDescr) + .end_cell(); + var config_addr = config_param(0).begin_parse().preload_uint(256); + send_validator_set_to_config(config_addr, vset, elect_at); + ;; add frozen to the dictionary of past elections + var past_elections = ds~load_dict(); + past_elections~udict_set_builder(32, elect_at, pack_past_election( + start + elect_for + stake_held, stake_held, vset.cell_hash(), + frozen, total_stakes, 0, null())); + ;; store credits and frozen until end + set_data(begin_cell() + .store_dict(elect) + .store_dict(credits) + .store_dict(past_elections) + .store_slice(ds) + .end_cell()); + return true; +} + +int update_active_vset_id() impure { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var cur_hash = config_param(34).cell_hash(); + if (cur_hash == active_hash) { + ;; validator set unchanged + return false; + } + if (active_id) { + ;; active_id becomes inactive + var (fs, f) = past_elections.udict_get?(32, active_id); + if (f) { + ;; adjust unfreeze time of this validator set + var unfreeze_time = fs~load_uint(32); + var fs0 = fs; + var (stake_held, hash) = (fs~load_uint(32), fs~load_uint(256)); + throw_unless(57, hash == active_hash); + unfreeze_time = now() + stake_held; + past_elections~udict_set_builder(32, active_id, begin_cell() + .store_uint(unfreeze_time, 32) + .store_slice(fs0)); + } + } + ;; look up new active_id by hash + var id = -1; + do { + (id, var fs, var f) = past_elections.udict_get_next?(32, id); + if (f) { + var (tm, hash) = (fs~load_uint(64), fs~load_uint(256)); + if (hash == cur_hash) { + ;; parse more of this record + var (dict, total_stake, bonuses) = (fs~load_dict(), fs~load_grams(), fs~load_grams()); + ;; transfer 1/8 of accumulated everybody's grams to this validator set as bonuses + var amount = (grams >> 3); + grams -= amount; + bonuses += amount; + ;; serialize back + past_elections~udict_set_builder(32, id, begin_cell() + .store_uint(tm, 64) + .store_uint(hash, 256) + .store_dict(dict) + .store_grams(total_stake) + .store_grams(bonuses) + .store_slice(fs)); + ;; found + f = false; + } + } + } until (~ f); + active_id = (id.null?() ? 0 : id); + active_hash = cur_hash; + store_data(elect, credits, past_elections, grams, active_id, active_hash); + return true; +} + +int cell_hash_eq?(cell vset, int expected_vset_hash) inline_ref { + return vset.null?() ? false : cell_hash(vset) == expected_vset_hash; +} + +int validator_set_installed(ds, elect, credits) impure { + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + ifnot (finished) { + ;; elections not finished yet + return false; + } + var past_elections = ds~load_dict(); + var (fs, f) = past_elections.udict_get?(32, elect_at); + ifnot (f) { + ;; no election data in dictionary + return false; + } + ;; recover validator set hash + var vset_hash = fs.skip_bits(64).preload_uint(256); + if (config_param(34).cell_hash_eq?(vset_hash) | config_param(36).cell_hash_eq?(vset_hash)) { + ;; this validator set has been installed, forget elections + set_data(begin_cell() + .store_int(false, 1) ;; forget current elections + .store_dict(credits) + .store_dict(past_elections) + .store_slice(ds) + .end_cell()); + update_active_vset_id(); + return true; + } + return false; +} + +int check_unfreeze() impure { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + int id = -1; + do { + (id, var fs, var f) = past_elections.udict_get_next?(32, id); + if (f) { + var unfreeze_at = fs~load_uint(32); + if ((unfreeze_at <= now()) & (id != active_id)) { + ;; unfreeze! + (credits, past_elections, var unused_prizes) = unfreeze_all(credits, past_elections, id); + grams += unused_prizes; + ;; unfreeze only one at time, exit loop + store_data(elect, credits, past_elections, grams, active_id, active_hash); + ;; exit loop + f = false; + } + } + } until (~ f); + return ~ id.null?(); +} + +int announce_new_elections(ds, elect, credits) { + var next_vset = config_param(36); ;; next validator set + ifnot (next_vset.null?()) { + ;; next validator set exists, no elections needed + return false; + } + var elector_addr = config_param(1).begin_parse().preload_uint(256); + var (my_wc, my_addr) = my_address().parse_std_addr(); + if ((my_wc + 1) | (my_addr != elector_addr)) { + ;; this smart contract is not the elections smart contract anymore, no new elections + return false; + } + var cur_vset = config_param(34); ;; current validator set + if (cur_vset.null?()) { + return false; + } + var (elect_for, elect_begin_before, elect_end_before, stake_held) = get_validator_conf(); + var cur_valid_until = cur_vset.begin_parse().skip_bits(8 + 32).preload_uint(32); + var t = now(); + var t0 = cur_valid_until - elect_begin_before; + if (t < t0) { + ;; too early for the next elections + return false; + } + ;; less than elect_before_begin seconds left, create new elections + if (t - t0 < 60) { + ;; pretend that the elections started at t0 + t = t0; + } + ;; get stake parameters + (_, var min_stake) = config_param(17).begin_parse().load_grams(); + ;; announce new elections + var elect_at = t + elect_begin_before; + ;; elect_at~dump(); + var elect_close = elect_at - elect_end_before; + elect = pack_elect(elect_at, elect_close, min_stake, 0, new_dict(), false, false); + set_data(begin_cell().store_dict(elect).store_dict(credits).store_slice(ds).end_cell()); + return true; +} + +() run_ticktock(int is_tock) impure { + ;; check whether an election is being conducted + var ds = get_data().begin_parse(); + var (elect, credits) = (ds~load_dict(), ds~load_dict()); + ifnot (elect.null?()) { + ;; have an active election + throw_if(0, conduct_elections(ds, elect, credits)); ;; elections conducted, exit + throw_if(0, validator_set_installed(ds, elect, credits)); ;; validator set installed, current elections removed + } else { + throw_if(0, announce_new_elections(ds, elect, credits)); ;; new elections announced, exit + } + throw_if(0, update_active_vset_id()); ;; active validator set id updated, exit + check_unfreeze(); +} + +;; Get methods + +;; returns active election id or 0 +int active_election_id() method_id { + var elect = get_data().begin_parse().preload_dict(); + return elect.null?() ? 0 : elect.begin_parse().preload_uint(32); +} + +;; checks whether a public key participates in current elections +int participates_in(int validator_pubkey) method_id { + var elect = get_data().begin_parse().preload_dict(); + if (elect.null?()) { + return 0; + } + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + var (mem, found) = members.udict_get?(256, validator_pubkey); + return found ? mem~load_grams() : 0; +} + +;; returns the list of all participants of current elections with their stakes +_ participant_list() method_id { + var elect = get_data().begin_parse().preload_dict(); + if (elect.null?()) { + return nil; + } + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + var l = nil; + var id = (1 << 255) + ((1 << 255) - 1); + do { + (id, var fs, var f) = members.udict_get_prev?(256, id); + if (f) { + l = cons([id, fs~load_grams()], l); + } + } until (~ f); + return l; +} + +;; returns the list of all participants of current elections with their data +_ participant_list_extended() method_id { + var elect = get_data().begin_parse().preload_dict(); + if (elect.null?()) { + return (0, 0, 0, 0, nil, 0, 0); + } + var (elect_at, elect_close, min_stake, total_stake, members, failed, finished) = elect.unpack_elect(); + var l = nil; + var id = (1 << 255) + ((1 << 255) - 1); + do { + (id, var cs, var f) = members.udict_get_prev?(256, id); + if (f) { + var (stake, time, max_factor, addr, adnl_addr) = (cs~load_grams(), cs~load_uint(32), cs~load_uint(32), cs~load_uint(256), cs~load_uint(256)); + cs.end_parse(); + l = cons([id, [stake, max_factor, addr, adnl_addr]], l); + } + } until (~ f); + return (elect_at, elect_close, min_stake, total_stake, l, failed, finished); +} + +;; computes the return stake +int compute_returned_stake(int wallet_addr) method_id { + var cs = get_data().begin_parse(); + (_, var credits) = (cs~load_dict(), cs~load_dict()); + var (val, f) = credits.udict_get?(256, wallet_addr); + return f ? val~load_grams() : 0; +} + +;; returns the list of past election ids +tuple past_election_ids() method_id { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var id = (1 << 32); + var list = null(); + do { + (id, var fs, var f) = past_elections.udict_get_prev?(32, id); + if (f) { + list = cons(id, list); + } + } until (~ f); + return list; +} + +tuple past_elections() method_id { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var id = (1 << 32); + var list = null(); + do { + (id, var fs, var found) = past_elections.udict_get_prev?(32, id); + if (found) { + list = cons([id, unpack_past_election(fs)], list); + } + } until (~ found); + return list; +} + +tuple past_elections_list() method_id { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var id = (1 << 32); + var list = null(); + do { + (id, var fs, var found) = past_elections.udict_get_prev?(32, id); + if (found) { + var (unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints) = unpack_past_election(fs); + list = cons([id, unfreeze_at, vset_hash, stake_held], list); + } + } until (~ found); + return list; +} + +_ complete_unpack_complaint(slice cs) inline_ref { + var (complaint, voters, vset_id, weight_remaining) = cs.unpack_complaint_status(); + var voters_list = null(); + var voter_id = (1 << 32); + do { + (voter_id, _, var f) = voters.udict_get_prev?(16, voter_id); + if (f) { + voters_list = cons(voter_id, voters_list); + } + } until (~ f); + return [[complaint.begin_parse().unpack_complaint()], voters_list, vset_id, weight_remaining]; +} + +cell get_past_complaints(int election_id) inline_ref method_id { + var (elect, credits, past_elections, grams, active_id, active_hash) = load_data(); + var (fs, found?) = past_elections.udict_get?(32, election_id); + ifnot (found?) { + return null(); + } + var (unfreeze_at, stake_held, vset_hash, frozen_dict, total_stake, bonuses, complaints) = unpack_past_election(fs); + return complaints; +} + +_ show_complaint(int election_id, int chash) method_id { + var complaints = get_past_complaints(election_id); + var (cs, found) = complaints.udict_get?(256, chash); + return found ? complete_unpack_complaint(cs) : null(); +} + +tuple list_complaints(int election_id) method_id { + var complaints = get_past_complaints(election_id); + int id = (1 << 255) + ((1 << 255) - 1); + var list = null(); + do { + (id, var cs, var found?) = complaints.udict_get_prev?(256, id); + if (found?) { + list = cons(pair(id, complete_unpack_complaint(cs)), list); + } + } until (~ found?); + return list; +} + +int complaint_storage_price(int bits, int refs, int expire_in) method_id { + ;; compute complaint storage/creation price + var (deposit, bit_price, cell_price) = get_complaint_prices(); + var pps = (bits + 1024) * bit_price + (refs + 2) * cell_price; + var paid = pps * expire_in + deposit; + return paid + (1 << 30); +} diff --git a/tests/Integrational.spec.ts b/tests/Integrational.spec.ts new file mode 100644 index 0000000..b129028 --- /dev/null +++ b/tests/Integrational.spec.ts @@ -0,0 +1,172 @@ +import { Blockchain,BlockchainSnapshot, createShardAccount,internal,SandboxContract,SendMessageResult,SmartContractTransaction,TreasuryContract } from "@ton-community/sandbox"; +import { Address, Cell, beginCell, toNano } from 'ton-core'; +import { compile } from '@ton-community/blueprint'; +import '@ton-community/test-utils'; +import { keyPairFromSeed, getSecureRandomBytes, getSecureRandomWords, KeyPair } from 'ton-crypto'; +import { JettonMinter as DAOJettonMinter, jettonContentToCell } from '../contracts/jetton_dao/wrappers/JettonMinter'; +import { Pool, PoolConfig } from '../wrappers/Pool'; +import { Controller } from '../wrappers/Controller'; +import { Elector } from "../wrappers/Elector"; +import { Config } from "../wrappers/Config"; +import { setConsigliere } from "../wrappers/PayoutMinter.compile"; +import { Conf, ControllerState, Errors, Op } from "../PoolConstants"; +import { buff2bigint, computedGeneric, getRandomTon } from "../utils"; +import { ElectorTest } from "../wrappers/ElectorTest"; +import { loadConfig } from "../wrappers/ValidatorUtils"; +import { ConfigTest } from "../wrappers/ConfigTest"; + +type Validator = { + wallet: SandboxContract, + keys: KeyPair +}; + +describe('Integrational tests', () => { + let bc: Blockchain; + let deployer:SandboxContract; + let snapStates:Map + let controller_code:Cell; + let config_code:Cell; + let elector_code:Cell; + let pool_code:Cell; + let payout_minter_code:Cell; + let payout_wallet_code:Cell; + let dao_minter_code:Cell; + let dao_wallet_code:Cell; + let dao_voting_code:Cell; + let dao_vote_keeper_code:Cell; + let poolJetton: SandboxContract; + let pool:SandboxContract; + let controller:SandboxContract; + let validator:Validator; + let elector:SandboxContract; + let config:SandboxContract; + let poolConfig:PoolConfig; + let initialState: BlockchainSnapshot; + let loadSnapshot:(snap:string) => Promise; + + + beforeAll(async () => { + bc = await Blockchain.create(); + deployer = await bc.treasury('deployer', {balance: toNano("1000000000")}); + controller_code = await compile('Controller'); + pool_code = await compile('Pool'); + await setConsigliere(deployer.address); + payout_minter_code = await compile('PayoutMinter'); + payout_wallet_code = await compile('PayoutWallet'); + dao_minter_code = await compile('DAOJettonMinter'); + dao_wallet_code = await compile('DAOJettonWallet'); + dao_vote_keeper_code = await compile('DAOVoteKeeper'); + dao_voting_code = await compile('DAOVoting'); + elector_code = await compile('Elector'); + config_code = await compile('Config'); + + validator = { + wallet: await bc.treasury('validator'), + keys: keyPairFromSeed(await getSecureRandomBytes(32)) + }; + + 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)); + poolConfig = { + pool_jetton : poolJetton.address, + pool_jetton_supply : 0n, + optimistic_deposit_withdrawals: 0n, + + sudoer : deployer.address, + governor : deployer.address, + interest_manager : deployer.address, + halter : deployer.address, + consigliere : deployer.address, + approver : deployer.address, + + controller_code : controller_code, + payout_wallet_code : payout_wallet_code, + pool_jetton_wallet_code : dao_wallet_code, + payout_minter_code : payout_minter_code, + vote_keeper_code : dao_vote_keeper_code, + }; + + const electorAddress = Address.parse('Ef8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM0vF'); + const configAddress = Address.parse('Ef9VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVbxn'); + + + await bc.setShardAccount(electorAddress, createShardAccount({ + address: electorAddress, + code: elector_code, + data: ElectorTest.emptyState(buff2bigint(loadConfig(bc.config).get(34)!.hash())), + balance: toNano('1000') + })); + await bc.setShardAccount(configAddress, createShardAccount({ + address: configAddress, + code: config_code, + data: ConfigTest.configState(bc.config), + balance: toNano('1000') + })); + + pool = bc.openContract(Pool.createFromConfig(poolConfig, pool_code)); + + await pool.sendDeploy(deployer.getSender(),Conf.minStorage + toNano('1')); + + await poolJetton.sendDeploy(deployer.getSender(), toNano('1.05')); + + loadSnapshot = async (name:string) => { + const state = snapStates.get(name); + if(!state) + throw(Error(`Can't find state ${name}\nCheck tests execution order`)); + await bc.loadFrom(state); + } + + snapStates = new Map(); + }); + + it('Deploy controller', async () => { + const res = await pool.sendRequestControllerDeploy(validator.wallet.getSender(), Conf.minStorage + toNano('1'), 0); + expect(res.transactions).toHaveTransaction({ + from: validator.wallet.address, + to: pool.address, + outMessagesCount: 1 + }); + const deployMsg = res.transactions[1].outMessages.get(0)!; + + if(deployMsg.info.type !== "internal") + throw(Error("Should be internal")); + + expect(res.transactions).toHaveTransaction({ + from: pool.address, + to: deployMsg.info.dest, + deploy: true, + success: true, + }); + + controller = bc.openContract(Controller.createFromAddress(deployMsg.info.dest)); + + const controllerData = await controller.getControllerData(); + + expect(controllerData.state).toEqual(ControllerState.REST); + expect(controllerData.validator).toEqualAddress(validator.wallet.address); + expect(controllerData.pool).toEqualAddress(pool.address); + }); + it('Deposit to pool', async () => { + const depo = getRandomTon(300000, 500000); + const dataBefore = await pool.getFullData(); + const res = await pool.sendDeposit(deployer.getSender(), depo); + const minter = bc.openContract(await pool.getDepositMinter()); + const wallet = await minter.getWalletAddress(deployer.address); + expect(res.transactions).toHaveTransaction({ + from: wallet, + to: deployer.address, + op: Op.jetton.transfer_notification, + // value: Do we need to check conversion rate here? + success: true + }); + + const dataAfter = await pool.getFullData(); + expect(dataAfter.requestedForDeposit).toEqual(depo - Conf.poolDepositFee); + expect(dataAfter.depositPayout).toEqualAddress(minter.address); + }); +}); diff --git a/utils.ts b/utils.ts index 89cbd3b..ef43fef 100644 --- a/utils.ts +++ b/utils.ts @@ -87,6 +87,73 @@ export const sendBulkMessage = async (msg: Message, } } +interface IAny {} +interface TupleReaderConstructor { + new (...args: any[]) : T + fromReader(rdr: TupleReader) : T; +} + +class TupleReaderFactory{ + private constructable: TupleReaderConstructor; + constructor(constructable: TupleReaderConstructor) { + this.constructable = constructable; + } + createObject(rdr: TupleReader) : T { + return this.constructable.fromReader(rdr); + } +} + +class LispIterator implements Iterator { + + private curItem:TupleReader | null; + private done:boolean; + private ctor: TupleReaderFactory; + + constructor(tuple:TupleReader | null, ctor: TupleReaderFactory) { + this.done = false; //tuple === null || tuple.remaining == 0; + this.curItem = tuple; + this.ctor = ctor; + } + + public next(): IteratorResult { + + this.done = this.curItem === null || this.curItem.remaining == 0; + let value: TupleReader; + if( ! this.done) { + const head = this.curItem!.readTuple(); + const tail = this.curItem!.readTupleOpt(); + + if(tail !== null) { + this.curItem = tail; + } + + value = head; + return {done: this.done, value: this.ctor.createObject(value)}; + } + else { + return {done: true, value: null} + } + } +} + +export class LispList { + private tuple: TupleReader | null; + private ctor: TupleReaderFactory; + + constructor(tuple: TupleReader | null, ctor: TupleReaderConstructor) { + this.tuple = tuple; + this.ctor = new TupleReaderFactory(ctor); + } + + toArray() : T[] { + return [...this]; + } + + [Symbol.iterator]() { + return new LispIterator(this.tuple, this.ctor); + } +} + export { differentAddress, }; diff --git a/wrappers/Config.compile.ts b/wrappers/Config.compile.ts new file mode 100644 index 0000000..0ae09c4 --- /dev/null +++ b/wrappers/Config.compile.ts @@ -0,0 +1,5 @@ +import { CompilerConfig } from '@ton-community/blueprint'; + +export const compile:CompilerConfig = { + targets: ['contracts/stdlib.func','elector/config-code.fc'] +} diff --git a/wrappers/Config.ts b/wrappers/Config.ts new file mode 100644 index 0000000..498210f --- /dev/null +++ b/wrappers/Config.ts @@ -0,0 +1,9 @@ +import { Address, Cell, Contract} from "ton-core"; + +export class Config implements Contract { + constructor(readonly address: Address,readonly init?: { code: Cell; data: Cell}){} + + static createFromAddress(address: Address) { + return new Config(address); + } +} diff --git a/wrappers/ConfigTest.ts b/wrappers/ConfigTest.ts new file mode 100644 index 0000000..b1affa2 --- /dev/null +++ b/wrappers/ConfigTest.ts @@ -0,0 +1,24 @@ +import { SandboxContractProvider, TickOrTock } from "@ton-community/sandbox"; +import { buff2bigint } from "../utils"; +import { Config } from "./Config"; +import { Address, Cell, ContractProvider, beginCell } from "ton-core"; +export class ConfigTest extends Config { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell, special:{tick:boolean, tock:boolean} }) { + super(address, init); + } + + static createFromAddress(address: Address) { + return new ConfigTest(address); + } + + static configState(config: Cell, public_key?: Buffer) { + const pubKey = public_key ? buff2bigint(public_key) : 0n; + return beginCell().storeRef(config).storeUint(0, 32).storeUint(pubKey, 256).storeDict(null).endCell() + } + + async sendTickTock(provider: SandboxContractProvider, which: TickOrTock) { + return await provider.tickTock(which); + } + + +} diff --git a/wrappers/Elector.compile.ts b/wrappers/Elector.compile.ts new file mode 100644 index 0000000..44c7f9d --- /dev/null +++ b/wrappers/Elector.compile.ts @@ -0,0 +1,5 @@ +import { CompilerConfig } from '@ton-community/blueprint'; + +export const compile:CompilerConfig = { + targets: ['contracts/stdlib.func', 'elector/elector-code.fc'] +}; diff --git a/wrappers/Elector.ts b/wrappers/Elector.ts new file mode 100644 index 0000000..850dc7f --- /dev/null +++ b/wrappers/Elector.ts @@ -0,0 +1,126 @@ +import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, toNano, Sender, SendMode, Tuple, TupleReader } from "ton-core"; +import { LispList, bigint2buff, buff2bigint } from "../utils"; +import { signData, loadConfig } from "./ValidatorUtils"; + +export class Participant { + constructor(readonly id: bigint, + readonly stake: bigint, + readonly max_factor: number, + readonly address: Address, + readonly adnl: bigint + ) { } + static fromReader(rdr: TupleReader) { + const id = rdr.readBigNumber(); + const data = rdr.readTuple(); + return new Participant(id, + data.readBigNumber(), + data.readNumber(), + new Address(-1, bigint2buff(data.readBigNumber())), + data.readBigNumber() + ); + } +} +export class Elector implements Contract { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell, special:{tick:boolean, tock:boolean} }) {} + + static createFromAddress(address: Address) { + return new Elector(address); + } + + static createFromConfig(address: Address, code: Cell, config: Cell) { + const confDict = loadConfig(config); + const data = beginCell() + .storeMaybeRef(null) + .storeMaybeRef(null) + .storeMaybeRef(null) + .storeCoins(0) + .storeUint(buff2bigint(confDict.get(34)!.hash()), 256) + .endCell(); + const init = {code, data, special:{tick: true, tock:true}}; + + return new Elector(address, init); + } + + static newStakeMessage(src: Address, + public_key: Buffer, + private_key: Buffer, + stake_at: number | bigint, + max_factor: number, + adnl_address: bigint, + query_id:bigint | number = 1) { + + const signCell = beginCell().storeUint(0x654c5074, 32) + .storeUint(stake_at, 32) + .storeUint(max_factor, 32) + .storeUint(buff2bigint(src.hash), 256) + .storeUint(adnl_address, 256) + .endCell() + + const signature = signData(signCell, private_key); + + return beginCell().storeUint(0x4e73744b, 32) + .storeUint(query_id, 64) + .storeUint(buff2bigint(public_key), 256) + .storeUint(stake_at, 32) + .storeUint(max_factor, 32) + .storeUint(adnl_address, 256) + .storeRef(signature) + .endCell(); + } + + + async sendNewStake(provider: ContractProvider, + via: Sender, + value: bigint, + src: Address, + public_key: Buffer, + private_key: Buffer, + stake_at: number | bigint, + max_factor: number = 1 << 16, + adnl_address: bigint = 0n, + query_id:bigint | number = 1) { + await provider.internal(via,{ + value, + body: Elector.newStakeMessage(src, + public_key, + private_key, + stake_at, + max_factor, + adnl_address, + query_id), + sendMode: SendMode.PAY_GAS_SEPARATELY + }); + } + + async getActiveElectionId(provider: ContractProvider) { + const { stack } = await provider.get('active_election_id', []) + return stack.readNumber() + } + + async getStake(provider: ContractProvider, validator_key: Buffer) { + const { stack } = await provider.get('participates_in', [ + { type: 'int', value: BigInt('0x'+ validator_key.toString('hex')) } + ]) + return stack.readBigNumber(); + } + + async getReturnedStake(provider: ContractProvider, wallet:Address) { + const { stack} = await provider.get('compute_returned_stake', [{ + type: 'int', value: buff2bigint(wallet.hash) + }]); + return stack.readBigNumber(); + } + + async getParticipantListExtended(provider: ContractProvider) { + const { stack } = await provider.get('participant_list_extended', []); + return { + elect_at: stack.readNumber(), + elect_close: stack.readNumber(), + min_stake: stack.readBigNumber(), + total_stake: stack.readBigNumber(), + list: new LispList(stack.readTupleOpt(), Participant).toArray(), + failed: stack.readBoolean(), + finished: stack.readBoolean() + }; + } +} diff --git a/wrappers/ElectorTest.ts b/wrappers/ElectorTest.ts new file mode 100644 index 0000000..dbefebd --- /dev/null +++ b/wrappers/ElectorTest.ts @@ -0,0 +1,60 @@ +import { Elector } from './Elector'; +import { Address, Cell, beginCell, Dictionary } from 'ton-core'; +import { loadConfig, getStakeConf, packElect, getElectionsConf } from './ValidatorUtils'; +import { Blockchain, BlockchainContractProvider, SandboxContractProvider, TickOrTock } from '@ton-community/sandbox'; + +export class ElectorTest extends Elector { + constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell, special:{tick:boolean, tock:boolean} }) { + super(address, init); + } + + + static emptyState(curHash: bigint = 0n) { + return beginCell() + .storeMaybeRef(null) + .storeMaybeRef(null) + .storeMaybeRef(null) + .storeCoins(0) + .storeUint(0, 32) + .storeUint(0, 256) + .endCell(); + } + + static electionsAnnounced(config:Cell,prev_state:Cell | null = null, now:number = Math.floor(Date.now() / 1000)) { + const confDict = loadConfig(config); + const elConf = getElectionsConf(config); + const sConf = getStakeConf(confDict); + const electAt = now + elConf.begin_before; + const electClose = electAt - elConf.end_before; + const elect = packElect(electAt, electClose, sConf.min_stake, 0n, null, false, false); + const curState = prev_state !== null ? prev_state : ElectorTest.emptyState(); + const ss = curState.beginParse(); + const oldElect = ss.loadMaybeRef(); + const oldCredits = ss.loadMaybeRef(); + + return beginCell().storeMaybeRef(elect).storeMaybeRef(oldCredits).storeSlice(ss).endCell(); + } + + static createFromAddress(address: Address) { + return new ElectorTest(address); + } + + static createFromCode(address: Address, code: Cell) { + const data = ElectorTest.emptyState(); + const init = {code, data, special:{tick: true, tock:true}}; + + return new ElectorTest(address, init); + } + + static createFromState(address: Address, code: Cell, config:Cell, state:'empty' | 'announced') { + const data = state == 'empty' ? ElectorTest.emptyState() : ElectorTest.electionsAnnounced(config); + const init = {code, data, special: {tick: true, tock:true}}; + + return new ElectorTest(address, init); + } + + async sendTickTock(provider: SandboxContractProvider, which: TickOrTock) { + await provider.tickTock(which); + } + +}