mirror of
https://github.com/danog/ton.git
synced 2024-12-02 09:28:02 +01:00
799 lines
27 KiB
Plaintext
799 lines
27 KiB
Plaintext
|
;; Elector smartcontract
|
||
|
|
||
|
;; cur_elect credits past_elect grams active_id active_hash
|
||
|
(cell, cell, cell, int, int, int) load_data() {
|
||
|
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_elect grams active_id active_hash
|
||
|
() store_data(elect, credits, past_elect, grams, active_id, active_hash) impure {
|
||
|
set_data(begin_cell()
|
||
|
.store_dict(elect)
|
||
|
.store_dict(credits)
|
||
|
.store_dict(past_elect)
|
||
|
.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) {
|
||
|
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) {
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
;; 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));
|
||
|
}
|
||
|
|
||
|
() send_message_back(addr, ans_tag, query_id, body, grams, mode) impure {
|
||
|
;; 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 {
|
||
|
return send_message_back(addr, 0xee6f454c, query_id, reason, 0, 64);
|
||
|
}
|
||
|
|
||
|
() send_confirmation(addr, query_id, comment) impure {
|
||
|
return send_message_back(addr, 0xf374484c, query_id, comment, 1000000000, 2);
|
||
|
}
|
||
|
|
||
|
() send_validator_set_to_config(config_addr, vset, query_id) impure {
|
||
|
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) {
|
||
|
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 {
|
||
|
var (src_wc, src_addr) = parse_std_addr(s_addr);
|
||
|
var ds = get_data().begin_parse();
|
||
|
var elect = ds~load_dict();
|
||
|
if (null?(elect) | (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) {
|
||
|
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) {
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
_ unfreeze_all(credits, past_elections, elect_id) {
|
||
|
var (fs, f) = past_elections~udict_delete_get?(32, elect_id);
|
||
|
ifnot (f) {
|
||
|
;; no elections with this id
|
||
|
return (credits, past_elections, 0);
|
||
|
}
|
||
|
var (data1, vset_hash, fdict, tot_stakes, bonuses, complaints) = (fs~load_uint(64), fs~load_uint(256), fs~load_dict(), fs~load_grams(), fs~load_grams(), fs~load_dict());
|
||
|
fs.end_parse();
|
||
|
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 {
|
||
|
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 {
|
||
|
var (elect, credits, past_elect, 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_elect, grams, active_id, active_hash);
|
||
|
}
|
||
|
;; zero source address -1:00..00 (collecting validator fees)
|
||
|
var (fs, f) = past_elect.udict_get?(32, active_id);
|
||
|
ifnot (f) {
|
||
|
;; active validator set not found (?)
|
||
|
grams += msg_value;
|
||
|
} else {
|
||
|
;; credit active validator set bonuses
|
||
|
var (data, hash, dict, total_stake, bonuses) = (fs~load_uint(64), fs~load_uint(256), fs~load_dict(), fs~load_grams(), fs~load_grams());
|
||
|
bonuses += msg_value;
|
||
|
past_elect~udict_set_builder(32, active_id, begin_cell()
|
||
|
.store_uint(data, 64)
|
||
|
.store_uint(hash, 256)
|
||
|
.store_dict(dict)
|
||
|
.store_grams(total_stake)
|
||
|
.store_grams(bonuses)
|
||
|
.store_slice(fs));
|
||
|
}
|
||
|
store_data(elect, credits, past_elect, grams, active_id, active_hash);
|
||
|
return ();
|
||
|
}
|
||
|
|
||
|
() recover_stake(op, s_addr, cs, query_id) impure {
|
||
|
(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) {
|
||
|
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);
|
||
|
;; run_method3(1666, s_addr, cs, query_id);
|
||
|
after_code_upgrade(s_addr, cs, query_id);
|
||
|
throw(0);
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
() 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);
|
||
|
}
|
||
|
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) {
|
||
|
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, 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(), 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(tuple4(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(), 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 vset = new_dict();
|
||
|
var frozen = new_dict();
|
||
|
do {
|
||
|
var (stake, max_f, pubkey, adnl_addr) = l~list_next().untuple4();
|
||
|
;; 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;
|
||
|
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, 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 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(0x11, 8) ;; validators#11
|
||
|
.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_slice(vdict.begin_parse()) ;; list:(Hashmap 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_elect = ds~load_dict();
|
||
|
past_elect~udict_set_builder(32, elect_at, begin_cell()
|
||
|
.store_uint(start + elect_for + stake_held, 32)
|
||
|
.store_uint(stake_held, 32)
|
||
|
.store_uint(cell_hash(vset), 256)
|
||
|
.store_dict(frozen)
|
||
|
.store_grams(total_stakes)
|
||
|
.store_grams(0)
|
||
|
.store_int(false, 1));
|
||
|
;; store credits and frozen until end
|
||
|
set_data(begin_cell()
|
||
|
.store_dict(elect)
|
||
|
.store_dict(credits)
|
||
|
.store_dict(past_elect)
|
||
|
.store_slice(ds)
|
||
|
.end_cell());
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int update_active_vset_id() impure {
|
||
|
var (elect, credits, past_elect, 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_elect.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_elect~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_elect.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_elect~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_elect, grams, active_id, active_hash);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
int cell_hash_eq?(cell vset, int expected_vset_hash) {
|
||
|
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_elect, grams, active_id, active_hash) = load_data();
|
||
|
int id = -1;
|
||
|
do {
|
||
|
(id, var fs, var f) = past_elect.udict_get_next?(32, id);
|
||
|
if (f) {
|
||
|
var unfreeze_at = fs~load_uint(32);
|
||
|
if ((unfreeze_at <= now()) & (id != active_id)) {
|
||
|
;; unfreeze!
|
||
|
(credits, past_elect, var unused_prizes) = unfreeze_all(credits, past_elect, id);
|
||
|
grams += unused_prizes;
|
||
|
;; unfreeze only one at time, exit loop
|
||
|
store_data(elect, credits, past_elect, 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(pair(id, fs~load_grams()), l);
|
||
|
}
|
||
|
} until (~ f);
|
||
|
return l;
|
||
|
}
|
||
|
|
||
|
;; 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;
|
||
|
}
|