1
0
Fork 0
mirror of https://github.com/ton-blockchain/ton synced 2025-02-15 04:32:21 +00:00
ton/crypto/func/auto-tests/legacy_tests/nominator-pool/pool.fc
EmelyanenkoK 6b49d6a382
Add legacy_tester for existing funC contracts (#588)
* Add legacy_tester for existing funC contracts

* Add storage-contracts and pragma options
2023-01-12 12:33:15 +03:00

746 lines
36 KiB
Text

;; The validator has his own wallet in the masterchain, on which he holds his own coins for operating.
;; From this wallet he sends commands to this nominator pool (mostly `new_stake`, `update_validator_set` and `recover_stake`).
;; Register/vote_for complaints and register/vote_for config proposals are sent from validator's wallet.
;;
;; Pool contract must be in masterchain.
;; Nominators' wallets must be in the basechain.
;; The validator in most cases have two pools (for even and odd validation rounds).
#include "stdlib.fc";
int op::new_stake() asm "0x4e73744b PUSHINT"; ;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L621
int op::new_stake_error() asm "0xee6f454c PUSHINT"; ;; return_stake https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L169
int op::new_stake_ok() asm "0xf374484c PUSHINT"; ;; send_confirmation https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L173
int op::recover_stake() asm "0x47657424 PUSHINT"; ;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L625
int op::recover_stake_error() asm "0xfffffffe PUSHINT"; ;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L407
int op::recover_stake_ok() asm "0xf96f7324 PUSHINT"; ;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L426
int ADDR_SIZE() asm "256 PUSHINT";
int BOUNCEABLE() asm "0x18 PUSHINT";
int NON_BOUNCEABLE() asm "0x10 PUSHINT";
int SEND_MODE_PAY_FEE_SEPARATELY() asm "1 PUSHINT"; ;; means that the sender wants to pay transfer fees separately
int SEND_MODE_IGNORE_ERRORS() asm "2 PUSHINT"; ;; means that any errors arising while processing this message during the action phase should be ignored
int SEND_MODE_REMAINING_AMOUNT() asm "64 PUSHINT"; ;; is used for messages that carry all the remaining value of the inbound message in addition to the value initially indicated in the new message
int ONE_TON() asm "1000000000 PUSHINT";
int MIN_TONS_FOR_STORAGE() asm "10000000000 PUSHINT"; ;; 10 TON
int DEPOSIT_PROCESSING_FEE() asm "1000000000 PUSHINT"; ;; 1 TON
int MIN_STAKE_TO_SEND() asm "500000000000 PUSHINT"; ;; 500 TON
int VOTES_LIFETIME() asm "2592000 PUSHINT"; ;; 30 days
int binary_log_ceil(int x) asm "UBITSIZE";
;; hex parse same with bridge https://github.com/ton-blockchain/bridge-func/blob/d03dbdbe9236e01efe7f5d344831bf770ac4c613/func/text_utils.fc
(slice, int) ~load_hex_symbol(slice comment) {
int n = comment~load_uint(8);
n = n - 48;
throw_unless(329, n >= 0);
if (n < 10) {
return (comment, (n));
}
n = n - 7;
throw_unless(329, n >= 0);
if (n < 16) {
return (comment, (n));
}
n = n - 32;
throw_unless(329, (n >= 0) & (n < 16));
return (comment, n);
}
(slice, int) ~load_text_hex_number(slice comment, int byte_length) {
int current_slice_length = comment.slice_bits() / 8;
int result = 0;
int counter = 0;
repeat (2 * byte_length) {
result = result * 16 + comment~load_hex_symbol();
counter = counter + 1;
if (counter == current_slice_length) {
if (comment.slice_refs() == 1) {
cell _cont = comment~load_ref();
comment = _cont.begin_parse();
current_slice_length = comment.slice_bits() / 8;
counter = 0;
}
}
}
return (comment, result);
}
slice make_address(int wc, int addr) inline_ref {
return begin_cell()
.store_uint(4, 3).store_int(wc, 8).store_uint(addr, ADDR_SIZE()).end_cell().begin_parse();
}
;; https://github.com/ton-blockchain/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/block.tlb#L584
int is_elector_address(int wc, int addr) inline_ref {
return (wc == -1) & (config_param(1).begin_parse().preload_uint(ADDR_SIZE()) == addr);
}
slice elector_address() inline_ref {
int elector = config_param(1).begin_parse().preload_uint(ADDR_SIZE());
return make_address(-1, elector);
}
;; https://github.com/ton-blockchain/ton/blob/ae5c0720143e231c32c3d2034cfe4e533a16d969/crypto/block/block.tlb#L721
int max_recommended_punishment_for_validator_misbehaviour(int stake) inline_ref {
cell cp = config_param(40);
if (cell_null?(cp)) {
return 101000000000; ;; 101 TON - https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/lite-client/lite-client.cpp#L3678
}
slice cs = cp.begin_parse();
(int prefix,
int default_flat_fine, int default_proportional_fine,
int severity_flat_mult, int severity_proportional_mult,
int unpunishable_interval,
int long_interval, int long_flat_mult, int long_proportional_mult) =
(cs~load_uint(8),
cs~load_coins(), cs~load_uint(32),
cs~load_uint(16), cs~load_uint(16),
cs~load_uint(16),
cs~load_uint(16), cs~load_uint(16), cs~load_uint(16)
);
;; https://github.com/ton-blockchain/ton/blob/master/lite-client/lite-client.cpp#L3721
int fine = default_flat_fine;
int fine_part = default_proportional_fine;
fine *= severity_flat_mult; fine >>= 8;
fine_part *= severity_proportional_mult; fine_part >>= 8;
fine *= long_flat_mult; fine >>= 8;
fine_part *= long_proportional_mult; fine_part >>= 8;
return min(stake, fine + muldiv(stake, fine_part, 1 << 32)); ;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L529
}
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/block/block.tlb#L632
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L118
int get_validator_config() inline_ref {
slice cs = config_param(15).begin_parse();
(int validators_elected_for, int elections_start_before, int elections_end_before, int stake_held_for) = (cs~load_uint(32), cs~load_uint(32), cs~load_uint(32), cs.preload_uint(32));
return stake_held_for;
}
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/block/block.tlb#L712
(int, int, cell) get_current_validator_set() inline_ref {
cell vset = config_param(34); ;; current validator set
slice cs = vset.begin_parse();
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/block/block.tlb#L579
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/config-code.fc#L49
throw_unless(9, cs~load_uint(8) == 0x12); ;; validators_ext#12 only
int utime_since = cs~load_uint(32); ;; actual start unixtime of current validation round
int utime_until = cs~load_uint(32); ;; supposed end unixtime of current validation round (utime_until = utime_since + validators_elected_for); unfreeze_at = utime_until + stake_held_for
return (utime_since, utime_until, vset);
}
;; check the validity of the new_stake message
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L208
int check_new_stake_msg(slice cs) impure inline_ref {
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();
return stake_at; ;; supposed start of next validation round (utime_since)
}
builder pack_nominator(int amount, int pending_deposit_amount) inline_ref {
return begin_cell().store_coins(amount).store_coins(pending_deposit_amount);
}
(int, int) unpack_nominator(slice ds) inline_ref {
return (
ds~load_coins(), ;; amount
ds~load_coins() ;; pending_deposit_amount
);
}
cell pack_config(int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake) inline_ref {
return begin_cell()
.store_uint(validator_address, ADDR_SIZE())
.store_uint(validator_reward_share, 16)
.store_uint(max_nominators_count, 16)
.store_coins(min_validator_stake)
.store_coins(min_nominator_stake)
.end_cell();
}
(int, int, int, int, int) unpack_config(slice ds) inline_ref {
return (
ds~load_uint(ADDR_SIZE()), ;; validator_address
ds~load_uint(16), ;; validator_reward_share
ds~load_uint(16), ;; max_nominators_count
ds~load_coins(), ;; min_validator_stake
ds~load_coins() ;; min_nominator_stake
);
}
() save_data(int state, int nominators_count, int stake_amount_sent, int validator_amount, cell config, cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) impure inline_ref {
set_data(begin_cell()
.store_uint(state, 8)
.store_uint(nominators_count, 16)
.store_coins(stake_amount_sent)
.store_coins(validator_amount)
.store_ref(config)
.store_dict(nominators)
.store_dict(withdraw_requests)
.store_uint(stake_at, 32)
.store_uint(saved_validator_set_hash, 256)
.store_uint(validator_set_changes_count, 8)
.store_uint(validator_set_change_time, 32)
.store_uint(stake_held_for, 32)
.store_dict(config_proposal_votings)
.end_cell());
}
(int, int, int, int, (int, int, int, int, int), cell, cell, int, int, int, int, int, cell) load_data() inline_ref {
slice ds = get_data().begin_parse();
return (
ds~load_uint(8), ;; state
ds~load_uint(16), ;; nominators_count
ds~load_coins(), ;; stake_amount_sent
ds~load_coins(), ;; validator_amount
unpack_config(ds~load_ref().begin_parse()), ;; config
ds~load_dict(), ;; nominators
ds~load_dict(), ;; withdraw_requests
ds~load_uint(32), ;; stake_at
ds~load_uint(256), ;; saved_validator_set_hash
ds~load_uint(8), ;; validator_set_changes_count
ds~load_uint(32), ;; validator_set_change_time
ds~load_uint(32), ;; stake_held_for
ds~load_dict() ;; config_proposal_votings
);
}
() send_msg(slice to_address, int amount, cell payload, int flags, int send_mode) impure inline_ref {
int has_payload = ~ cell_null?(payload);
builder msg = begin_cell()
.store_uint(flags, 6)
.store_slice(to_address)
.store_coins(amount)
.store_uint(has_payload ? 1 : 0, 1 + 4 + 4 + 64 + 32 + 1 + 1);
if (has_payload) {
msg = msg.store_ref(payload);
}
send_raw_message(msg.end_cell(), send_mode);
}
() send_excesses(slice sender_address) impure inline_ref {
send_msg(sender_address, 0, null(), NON_BOUNCEABLE(), SEND_MODE_REMAINING_AMOUNT() + SEND_MODE_IGNORE_ERRORS()); ;; non-bouneable, remaining inbound message amount, fee deducted from amount, ignore errors
}
(cell, cell, int, int) withdraw_nominator(int address, cell nominators, cell withdraw_requests, int balance, int nominators_count) impure inline_ref {
(slice nominator, int found) = nominators.udict_get?(ADDR_SIZE(), address);
throw_unless(60, found);
(int amount, int pending_deposit_amount) = unpack_nominator(nominator);
int withdraw_amount = amount + pending_deposit_amount;
if (withdraw_amount > balance - MIN_TONS_FOR_STORAGE()) {
return (nominators, withdraw_requests, balance, nominators_count);
}
nominators~udict_delete?(ADDR_SIZE(), address);
withdraw_requests~udict_delete?(ADDR_SIZE(), address);
nominators_count -= 1;
balance -= withdraw_amount;
if (withdraw_amount >= ONE_TON()) {
send_msg(make_address(0, address), withdraw_amount, null(), NON_BOUNCEABLE(), 0); ;; non-bouneable, fee deducted from amount, revert on errors
}
return (nominators, withdraw_requests, balance, nominators_count);
}
(cell, cell, int, int) process_withdraw_requests(cell nominators, cell withdraw_requests, int balance, int nominators_count, int limit) impure inline_ref {
int count = 0;
int address = -1;
int need_break = 0;
do {
(address, slice cs, int f) = withdraw_requests.udict_get_next?(ADDR_SIZE(), address);
if (f) {
(nominators, withdraw_requests, int new_balance, nominators_count) = withdraw_nominator(address, nominators, withdraw_requests, balance, nominators_count);
need_break = (new_balance == balance);
balance = new_balance;
count += 1;
if (count >= limit) {
need_break = -1;
}
}
} until ((~ f) | (need_break));
return (nominators, withdraw_requests, nominators_count, balance);
}
int calculate_total_nominators_amount(cell nominators) inline_ref {
int total = 0;
int address = -1;
do {
(address, slice cs, int f) = nominators.udict_get_next?(ADDR_SIZE(), address);
if (f) {
(int amount, int pending_deposit_amount) = unpack_nominator(cs);
total += (amount + pending_deposit_amount);
}
} until (~ f);
return total;
}
cell distribute_share(int reward, cell nominators) inline_ref {
int total_amount = 0;
int address = -1;
do {
(address, slice cs, int f) = nominators.udict_get_next?(ADDR_SIZE(), address);
if (f) {
(int amount, int pending_deposit_amount) = unpack_nominator(cs);
total_amount += amount;
}
} until (~ f);
cell new_nominators = new_dict();
address = -1;
do {
(address, slice cs, int f) = nominators.udict_get_next?(ADDR_SIZE(), address);
if (f) {
(int amount, int pending_deposit_amount) = unpack_nominator(cs);
if (total_amount > 0) {
amount += muldiv(reward, amount, total_amount);
if (amount < 0) {
amount = 0;
}
}
amount += pending_deposit_amount;
new_nominators~udict_set_builder(ADDR_SIZE(), address, pack_nominator(amount, 0));
}
} until (~ f);
return new_nominators;
}
() recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure {
int balance = pair_first(get_balance());
slice cs = in_msg_full.begin_parse();
int flags = cs~load_uint(4);
slice sender_address = cs~load_msg_addr();
(int sender_wc, int sender_addr) = parse_std_addr(sender_address);
if (flags & 1) { ;; bounced messages
if (in_msg_body.slice_bits() >= 64) {
in_msg_body~skip_bits(32); ;; skip 0xFFFFFFFF bounced prefix
int op = in_msg_body~load_uint(32);
if ((op == op::new_stake()) & (is_elector_address(sender_wc, sender_addr))) {
;; `new_stake` from nominator-pool should always be handled without throws by elector
;; because nominator-pool do `check_new_stake_msg` and `msg_value` checks before sending `new_stake`.
;; If the stake is not accepted elector will send `new_stake_error` response message.
;; Nevertheless we do process theoretically possible bounced `new_stake`.
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
if (state == 1) {
state = 0;
}
save_data(
state,
nominators_count,
stake_amount_sent,
validator_amount,
pack_config(validator_address, validator_reward_share, max_nominators_count, min_validator_stake, min_nominator_stake),
nominators,
withdraw_requests,
stake_at,
saved_validator_set_hash,
validator_set_changes_count,
validator_set_change_time,
stake_held_for,
config_proposal_votings
);
}
}
return (); ;; ignore other bounces messages
}
int op = in_msg_body~load_uint(32);
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
if (op == 0) {
;; We use simple text comments for nominator operations so nominators can do it from any wallet app.
;; In other cases, they will need to put a stake on a browser extension, or use scripts, which can be inconvenient.
;; Throw on any unexpected request so that the stake is bounced back to the nominator in case of a typo.
int action = in_msg_body~load_uint(8);
int is_vote = (action == 121) | (action == 110); ;; "y" or "n"
throw_unless(64, (action == 100) | (action == 119) | is_vote); ;; "d" or "w" or "y" or "n"
if (~ is_vote) {
in_msg_body.end_parse();
throw_unless(61, sender_wc == 0); ;; nominators only in basechain
throw_unless(62, sender_addr != validator_address);
}
if (action == 100) { ;; "d" - deposit nominator (any time, will take effect in the next round)
(slice nominator, int found) = nominators.udict_get?(ADDR_SIZE(), sender_addr);
if (~ found) {
nominators_count += 1;
}
throw_unless(65, nominators_count <= max_nominators_count);
msg_value -= DEPOSIT_PROCESSING_FEE();
throw_unless(66, msg_value > 0);
(int amount, int pending_deposit_amount) = found ? unpack_nominator(nominator) : (0, 0);
if (state == 0) {
amount += msg_value;
} else {
pending_deposit_amount += msg_value;
}
throw_unless(67, amount + pending_deposit_amount >= min_nominator_stake);
throw_unless(68, cell_depth(nominators) < max(5, binary_log_ceil(nominators_count) * 2) ); ;; prevent dict depth ddos
nominators~udict_set_builder(ADDR_SIZE(), sender_addr, pack_nominator(amount, pending_deposit_amount));
}
if (action == 119) { ;; "w" - withdraw request (any time)
if (state == 0) {
(nominators, withdraw_requests, int new_balance, nominators_count) = withdraw_nominator(sender_addr, nominators, withdraw_requests, balance, nominators_count);
if (new_balance - msg_value >= MIN_TONS_FOR_STORAGE()) {
send_excesses(sender_address);
}
} else {
(slice nominator, int found) = nominators.udict_get?(ADDR_SIZE(), sender_addr);
throw_unless(69, found);
withdraw_requests~udict_set_builder(ADDR_SIZE(), sender_addr, begin_cell());
send_excesses(sender_address);
}
}
if (is_vote) {
int authorized = (sender_wc == -1) & (sender_addr == validator_address);
if (~ authorized) {
throw_unless(121, sender_wc == 0);
(slice nominator, authorized) = nominators.udict_get?(ADDR_SIZE(), sender_addr);
throw_unless(122, authorized);
(int amount, int pending_deposit_amount) = unpack_nominator(nominator);
throw_unless(123, amount > 0);
}
int proposal_hash = in_msg_body~load_text_hex_number(32);
in_msg_body.end_parse();
int support = action == 121;
(slice votes_slice, int found) = config_proposal_votings.udict_get?(256, proposal_hash);
if (~ found) {
;; require higher fee to prevent dictionary spam
int fee = ONE_TON();
int power = cell_depth(config_proposal_votings);
repeat (power) {
fee = muldiv(fee, 15, 10);
}
throw_unless(123, msg_value >= fee);
}
(cell votes_dict, int votes_create_time) = found ? (votes_slice~load_dict(), votes_slice~load_uint(32)) : (new_dict(), now());
(_, int vote_found) = votes_dict.udict_get?(256, sender_addr);
throw_if(124, vote_found);
votes_dict~udict_set_builder(256, sender_addr, begin_cell().store_int(support, 1).store_uint(now(), 32));
builder new_votes = begin_cell().store_dict(votes_dict).store_uint(votes_create_time, 32);
config_proposal_votings~udict_set_builder(256, proposal_hash, new_votes);
if (found) {
send_excesses(sender_address);
}
}
} else {
int query_id = in_msg_body~load_uint(64);
if (is_elector_address(sender_wc, sender_addr)) { ;; response from elector
accept_message();
if (op == op::recover_stake_ok()) {
state = 0;
int reward = msg_value - stake_amount_sent;
int nominators_reward = 0;
if (reward <= 0) {
validator_amount += reward;
if (validator_amount < 0) {
;; even this should never happen
nominators_reward = validator_amount;
validator_amount = 0;
}
} else {
int validator_reward = (reward * validator_reward_share) / 10000;
if (validator_reward > reward) { ;; Theoretical invalid case if validator_reward_share > 10000
validator_reward = reward;
}
validator_amount += validator_reward;
nominators_reward = reward - validator_reward;
}
nominators = distribute_share(nominators_reward, nominators); ;; call even if there was no reward to process deposit requests
stake_amount_sent = 0;
}
if (state == 1) {
if (op == op::new_stake_error()) { ;; error when new_stake; stake returned
state = 0;
}
if (op == op::new_stake_ok()) {
state = 2;
}
}
;; else just accept coins from elector
} else {
;; throw on any unexpected request so that the coins is bounced back to the sender in case of a typo
throw_unless(70, ((op >= 1) & (op <= 7)) | (op == op::recover_stake()) | (op == op::new_stake()));
if (op == 1) {
;; just accept coins
}
if (op == 2) { ;; process withdraw requests (at any time while the balance is enough)
int limit = in_msg_body~load_uint(8);
(nominators, withdraw_requests, nominators_count, int new_balance) = process_withdraw_requests(nominators, withdraw_requests, balance, nominators_count, limit);
if (new_balance - msg_value >= MIN_TONS_FOR_STORAGE()) {
send_excesses(sender_address);
}
}
if (op == 3) { ;; emergency process withdraw request (at any time if the balance is enough)
int request_address = in_msg_body~load_uint(ADDR_SIZE());
(slice withdraw_request, int found) = withdraw_requests.udict_get?(ADDR_SIZE(), request_address);
throw_unless(71, found);
(nominators, withdraw_requests, int new_balance, nominators_count) = withdraw_nominator(request_address, nominators, withdraw_requests, balance, nominators_count);
if (new_balance - msg_value >= MIN_TONS_FOR_STORAGE()) {
send_excesses(sender_address);
}
}
if (op == 6) { ;; update current valudator set hash (anyone can invoke)
throw_unless(113, validator_set_changes_count < 3);
(int utime_since, int utime_until, cell vset) = get_current_validator_set();
int current_hash = cell_hash(vset);
if (saved_validator_set_hash != current_hash) {
saved_validator_set_hash = current_hash;
validator_set_changes_count += 1;
validator_set_change_time = now();
}
send_excesses(sender_address);
}
if (op == 7) { ;; clean up outdating votings
int t = now();
int proposal_hash = -1;
do {
(proposal_hash, slice votes_slice, int found) = config_proposal_votings.udict_get_next?(256, proposal_hash);
if (found) {
(cell votes_dict, int votes_create_time) = (votes_slice~load_dict(), votes_slice~load_uint(32));
if (t - votes_create_time > VOTES_LIFETIME()) {
config_proposal_votings~udict_delete?(256, proposal_hash);
}
}
} until (~ found);
send_excesses(sender_address);
}
if (op == op::recover_stake()) { ;; send recover_stake to elector (anyone can send)
;; We need to take all credits from the elector at once,
;; because if we do not take all at once, then it will be processed as a fine by pool.
;; In the elector, credits (`credit_to`) are accrued in three places:
;; 1) return of surplus stake in elections (`try_elect`)
;; 2) reward for complaint when punish (`punish`) - before unfreezing
;; 3) unfreeze round (`unfreeze_without_bonuses`/`unfreeze_with_bonuses`)
;; We need to be guaranteed to wait for unfreezing round and only then send `recover_stake`.
;; So we are waiting for the change of 3 validator sets.
;; ADDITIONAL NOTE:
;; In a special case (if the network was down), the config theoretically can refuse the elector to save a new round after election - https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/config-code.fc#L494
;; and the elector will start a new election - https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L364
;; in this case, our pool will have to skip the round, but it will be able to recover stake later
throw_unless(111, validator_set_changes_count >= 2);
throw_unless(112, (validator_set_changes_count > 2) | (now() - validator_set_change_time > stake_held_for + 60));
;; https://github.com/ton-blockchain/ton/blob/b38d227a469666d83ac535ad2eea80cb49d911b8/crypto/smartcont/elector-code.fc#L887
cell payload = begin_cell().store_uint(op::recover_stake(), 32).store_uint(query_id, 64).end_cell();
send_msg(elector_address(), 0, payload, BOUNCEABLE(), SEND_MODE_REMAINING_AMOUNT()); ;; bounceable, carry all the remaining value of the inbound message, fee deducted from amount, revert on errors
}
;; message from validator
if (op == 4) { ;; deposit validator (any time)
throw_unless(73, (sender_wc == -1) & (sender_addr == validator_address));
msg_value -= DEPOSIT_PROCESSING_FEE();
throw_unless(74, msg_value > 0);
validator_amount += msg_value;
}
if (op == 5) { ;; withdraw validator (after recover_stake and before new_stake)
throw_unless(74, state == 0); ;; no withdraw request because validator software can wait right time
throw_unless(75, (sender_wc == -1) & (sender_addr == validator_address));
int request_amount = in_msg_body~load_coins();
throw_unless(78, request_amount > 0);
int total_nominators_amount = calculate_total_nominators_amount(nominators);
;; the validator can withdraw everything that does not belong to the nominators
throw_unless(76, request_amount <= balance - MIN_TONS_FOR_STORAGE() - total_nominators_amount);
validator_amount -= request_amount;
if (validator_amount < 0) {
validator_amount = 0;
}
send_msg(make_address(-1, validator_address), request_amount, null(), NON_BOUNCEABLE(), 0); ;; non-bouneable, fee deducted from amount, revert on errors
int new_balance = balance - request_amount;
if (new_balance - msg_value >= MIN_TONS_FOR_STORAGE()) {
send_excesses(sender_address);
}
}
if (op == op::new_stake()) {
throw_unless(78, (sender_wc == -1) & (sender_addr == validator_address));
throw_unless(79, state == 0);
throw_unless(80, query_id); ;; query_id must be greater then 0 to receive confirmation message from elector
throw_unless(86, msg_value >= ONE_TON()); ;; must be greater then new_stake sending to elector fee
int value = in_msg_body~load_coins();
slice msg = in_msg_body;
stake_at = check_new_stake_msg(in_msg_body);
stake_amount_sent = value - ONE_TON();
throw_unless(81, value >= MIN_STAKE_TO_SEND());
throw_unless(82, value <= balance - MIN_TONS_FOR_STORAGE());
throw_unless(83, validator_amount >= min_validator_stake);
throw_unless(84, validator_amount >= max_recommended_punishment_for_validator_misbehaviour(stake_amount_sent));
throw_unless(85, cell_null?(withdraw_requests)); ;; no withdraw requests
state = 1;
(int utime_since, int utime_until, cell vset) = get_current_validator_set();
saved_validator_set_hash = cell_hash(vset); ;; current validator set, we will be in next validator set
validator_set_changes_count = 0;
validator_set_change_time = utime_since;
stake_held_for = get_validator_config(); ;; save `stake_held_for` in case the config changes in the process
send_msg(elector_address(), value, begin_cell().store_uint(op, 32).store_uint(query_id, 64).store_slice(msg).end_cell(), BOUNCEABLE(), SEND_MODE_PAY_FEE_SEPARATELY()); ;; pay fee separately, rever on errors
}
}
}
save_data(
state,
nominators_count,
stake_amount_sent,
validator_amount,
pack_config(validator_address, validator_reward_share, max_nominators_count, min_validator_stake, min_nominator_stake),
nominators,
withdraw_requests,
stake_at,
saved_validator_set_hash,
validator_set_changes_count,
validator_set_change_time,
stake_held_for,
config_proposal_votings
);
}
;; Get methods
_ get_pool_data() method_id {
return load_data();
}
int has_withdraw_requests() method_id {
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
return ~ cell_null?(withdraw_requests);
}
(int, int, int) get_nominator_data(int nominator_address) method_id {
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
(slice nominator, int found) = nominators.udict_get?(ADDR_SIZE(), nominator_address);
throw_unless(86, found);
(int amount, int pending_deposit_amount) = unpack_nominator(nominator);
(slice withdraw_request, int withdraw_found) = withdraw_requests.udict_get?(ADDR_SIZE(), nominator_address);
return (amount, pending_deposit_amount, withdraw_found);
}
int get_max_punishment(int stake) method_id {
return max_recommended_punishment_for_validator_misbehaviour(stake);
}
tuple list_nominators() method_id {
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
var list = null();
int address = -1;
do {
(address, slice nominator, int found) = nominators.udict_get_next?(ADDR_SIZE(), address);
if (found) {
(int amount, int pending_deposit_amount) = unpack_nominator(nominator);
(_, int withdraw_requested) = withdraw_requests.udict_get?(ADDR_SIZE(), address);
list = cons(tuple4(address, amount, pending_deposit_amount, withdraw_requested), list);
}
} until (~ found);
return list;
}
tuple list_votes() method_id {
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
var list = null();
int proposal_hash = -1;
do {
(proposal_hash, slice votes_slice, int found) = config_proposal_votings.udict_get_next?(256, proposal_hash);
if (found) {
(cell votes_dict, int votes_create_time) = (votes_slice~load_dict(), votes_slice~load_uint(32));
list = cons(pair(proposal_hash, votes_create_time), list);
}
} until (~ found);
return list;
}
tuple list_voters(int proposal_hash) method_id {
(int state, int nominators_count, int stake_amount_sent, int validator_amount, (int validator_address, int validator_reward_share, int max_nominators_count, int min_validator_stake, int min_nominator_stake), cell nominators, cell withdraw_requests, int stake_at, int saved_validator_set_hash, int validator_set_changes_count, int validator_set_change_time, int stake_held_for, cell config_proposal_votings) = load_data();
var list = null();
(slice votes_slice, int found) = config_proposal_votings.udict_get?(256, proposal_hash);
throw_unless(133, found);
cell votes_dict = votes_slice~load_dict();
int address = -1;
do {
(address, slice cs, int found) = votes_dict.udict_get_next?(ADDR_SIZE(), address);
if (found) {
(int support, int vote_time) = (cs~load_int(1), cs~load_uint(32));
list = cons(triple(address, support, vote_time), list);
}
} until (~ found);
return list;
}