В данном туториале мы разберем смарт-контракт лотерею-розыгрыш. Смарт-контракт хорош тем, что:
- грамотно используется рандом. Подробнее про техническую часть рандома в TON здесь
- есть важные механики управления балансом контракта, которые можно будет применить в ваших смарт-контрактах
- удобная групировка и структура проекта, патерн которой стоит перенять.
Генерация случайных чисел — может понадобиться во многих разных проектах. В документации FunC есть функция random()
, но использовать её в реальных проектах нельзя!!! Ее результат можно легко предсказать, если вы не примените некоторые дополнительные приемы.
Чтобы сделать генерацию случайных чисел непредсказуемой, вы можете добавить к начальному значению текущее логическое время, чтобы разные транзакции имели разные начальные значения и результаты.
Сделать это можно с помощью randomize_lt()
, например:
randomize_lt();
int x = random(); ;; users can't predict this number
Также можно использовать randomize()
, прокинув туда несколько парметров включая в себя логическое время.
() randomize(int x) impure asm "ADDRAND";
В данной статье мы рассмотрим контракт розыгрыш, который использует randomize()
Контракт принимает только сообщения с 1 TON (остальные суммы будут возвращены). Смарт-контракт генерирует случайное число от 0 до 10000, чтобы определить, выиграете вы или нет.
Если человек выигрывает, его выигрыш уменьшается на нашу комиссию в размере 10%. Остальная часть контрактного баланса остается нетронутой. Победитель получит сообщение о выигрыше в комментарии к транзакции.
- 0,1% на выигрыш джекпота (половина баланса контракта) [0; 10)
- 9,9%, на выигрышь 5 TON. [10; 1000)
- 10% на выигрыш 2 TON. [1000; 2000)
Если нет выигрыша, ничего не происходит. [2000; 9999]
Код контракта - https://github.com/Vudi/new-year-ruffle/tree/main
Смарт-контракт представляет собой обработчик внутренних сообщений recv_internal
, который если тело сообщения пустое, запускает лоттерею/розыгрыш или же выполняет одно из условий по op
:
op == op::add_balance()
добавление баланса, на случай если в контракте закончаться деньгиop == op::maintain()
позволяет отправить из контракта внутренее сообщение с разным режимом, т.е позволяет управлять балансом смарт-контракта, а также если что позволит его уничтожить(сообщение сmode == 128 + 32
)op == op::withdraw()
позволяет достать часть денег из смарт-контракта - накопленную комиссию
В смарт-контракте, который мы рассматриваем, хорошая стилистика:
- смарт-контракт грамотно разнесен на несколько файлов, таким образом, что его очень удобно читать
- работа с хранилищем(речь о регистре
с4
конечно же) комбинируется с глобальными переменными, что опять же улучшает читаемость кода, делая смарт-контракт понятным
Команды op
и часто используемая переменная в 1 TON вынесена в отдельный файл const.func
:
int op::maintain() asm "1001 PUSHINT";
int op::withdraw() asm "1002 PUSHINT";
int op::add_balance() asm "1003 PUSHINT";
int exit::invalid_bet() asm "2001 PUSHINT";
int 1ton() asm "1000000000 PUSHINT";
В файл admin.func
вынесены админские команды, adm::maintain
, которая позволяет отправить сообщение от смарт-контракта с любым mode - т.е позволяет управлять балансом смарт-контракта:
() adm::maintain(slice in_msg_body) impure inline_ref {
int mode = in_msg_body~load_uint(8);
send_raw_message(in_msg_body~load_ref(), mode);
}
И adm::withdraw()
позволяющая вытащить часть денег удобным способом:
() adm::withdraw() impure inline_ref {
cell body = begin_cell()
.store_uint(0, 32)
.store_slice(msg::commission_withdraw())
.end_cell();
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(db::admin_addr)
.store_coins(db::service_balance)
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(body)
.end_cell();
db::service_balance = 0;
send_raw_message(msg, 0);
}
Смарт-контракт отправляет сообщения при пройгрыше и победе, они вынесенны в отдельный файл msg.func
, заметьте, что они являются типом slice
:
slice msg::commission_withdraw() asm "<b 124 word Withdraw commission| $, b> <s PUSHSLICE";
slice msg::jackpot() asm "<b 124 word Congrats! You have won jackpot!| $, b> <s PUSHSLICE";
slice msg::x2() asm "<b 124 word Congrats! You have won x2!| $, b> <s PUSHSLICE";
slice msg::x5() asm "<b 124 word Congrats! You have won x5!| $, b> <s PUSHSLICE";
В game.func
расположена логика розыгрыша/лотереи, код данного файла мы рассмотрим детально, но позже. В смарт-контракте предусмотрен Get-метод, который возвращает информацию из регистра с4
смарт-контракта. Хранится этот метод в файле get-methods.func
:
(int, int, (int, int), int, int) get_info() method_id {
init_data();
return (db::available_balance, db::service_balance, parse_std_addr(db::admin_addr), db::last_number, db::hash);
}
И наконец-то работа с хранилищем в файле storage.func
. Здесь важно отметить, что данные не сохраняются постоянно в регистр с4, а сначала они сохраняются в глобальные переменные, а потом в конце некоторого логического кода происходит сохранение в регистр c помощью функции pack_data()
:
global int init?;
global int db::available_balance;
global int db::service_balance;
global slice db::admin_addr;
global int db::last_number;
global int db::hash;
() init_data() impure {
ifnot(null?(init?)) {
throw(0x123);
}
slice ds = get_data().begin_parse();
db::available_balance = ds~load_coins();
db::service_balance = ds~load_coins();
db::admin_addr = ds~load_msg_addr();
db::last_number = ds~load_uint(64);
db::hash = slice_empty?(ds) ? 0 : ds~load_uint(256);
init? = true;
}
() pack_data() impure {
set_data(
begin_cell()
.store_coins(db::available_balance)
.store_coins(db::service_balance)
.store_slice(db::admin_addr)
.store_uint(db::last_number, 64)
.store_uint(db::hash, 256)
.end_cell()
);
}
Файл main.fc
начинается с импорта файлов, по которым мы прошлись выше:
#include "lib/stdlib.func";
#include "struct/const.func";
#include "struct/storage.func";
#include "struct/msg.func";
#include "struct/game.func";
#include "struct/admin.func";
#include "struct/get-methods.func";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
}
Достаем сообщение и функцией slice_hash создаем хэш, который далее будем использовать для рандома. Также как вы помните, любое сообщение начинается с флагов, сделаем небольшую проверку:
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int hash = slice_hash(cs);
throw_if(0, cs~load_uint(4) & 1);
}
Инициализируем данные с помощью вспомогательной функции из файла storage.func
и здесь же достанем адрес из сообщения:
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int hash = slice_hash(cs);
throw_if(0, cs~load_uint(4) & 1);
init_data();
slice sender_addr = cs~load_msg_addr();
}
Кажется, что дальше логично было бы достать op
, но для удобства использования смарт-контракта, основной функционал используется без op
, таким образом пользователь просто отправляет пустое сообщение в контракт и розыгрыш/лотерея начинается. Чтобы реализовать подобный функционал, просто проверяем оставшееся сообщение, если оно пустое, запускаем игру.
#include "lib/stdlib.func";
#include "struct/const.func";
#include "struct/storage.func";
#include "struct/msg.func";
#include "struct/game.func";
#include "struct/admin.func";
#include "struct/get-methods.func";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int hash = slice_hash(cs);
throw_if(0, cs~load_uint(4) & 1);
init_data();
slice sender_addr = cs~load_msg_addr();
if (in_msg_body.slice_empty?()) {
game::start(sender_addr, msg_value, hash);
pack_data();
throw(0);
}
}
Внутри функции игры мы будем менять данные, которые позже нужно будет сохранить в регистр хранения постоянных данных с4
, внутри функции менятются глобальные переменные, а в recv_internal()
мы сохраняем:
#include "lib/stdlib.func";
#include "struct/const.func";
#include "struct/storage.func";
#include "struct/msg.func";
#include "struct/game.func";
#include "struct/admin.func";
#include "struct/get-methods.func";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int hash = slice_hash(cs);
throw_if(0, cs~load_uint(4) & 1);
init_data();
slice sender_addr = cs~load_msg_addr();
if (in_msg_body.slice_empty?()) {
game::start(sender_addr, msg_value, hash);
pack_data();
throw(0);
}
}
Здесь может возникнуть вопрос, зачем вызывается исключение, после того как все отработало правильно. В соответствии с документацией по TVM пункт 4.5.1 для исключений есть зарезервированные коды 0–31, совпадающие с exit_code, а значит 0 - cтандартный код завершения успешного выполнения.
Дальше все просто, op
, смысл которых мы разобрали выше, итоговый код main.func
:
#include "lib/stdlib.func";
#include "struct/const.func";
#include "struct/storage.func";
#include "struct/msg.func";
#include "struct/game.func";
#include "struct/admin.func";
#include "struct/get-methods.func";
() recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure {
slice cs = in_msg_full.begin_parse();
int hash = slice_hash(cs);
throw_if(0, cs~load_uint(4) & 1);
init_data();
slice sender_addr = cs~load_msg_addr();
if (in_msg_body.slice_empty?()) {
game::start(sender_addr, msg_value, hash);
pack_data();
throw(0);
}
int op = in_msg_body~load_uint(32);
int is_admin = equal_slices(sender_addr, db::admin_addr);
if (op == op::add_balance()) {
db::available_balance += msg_value;
pack_data();
throw(0);
}
if (op == op::maintain()) {
throw_if(0xfffe, is_admin == 0);
adm::maintain(in_msg_body);
throw(0);
}
if (op == op::withdraw()) {
throw_if(0xfffd, is_admin == 0);
adm::withdraw();
pack_data();
throw(0);
}
throw(0xffff);
}
Наконец-то мы добрались до логики игры, файл game.func
начинается со вспомогательной функции выплаты приза:
() game::payout(slice sender_addr, int amount, slice msg) impure inline_ref {
cell body = begin_cell()
.store_uint(0, 32)
.store_slice(msg)
.end_cell();
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(sender_addr)
.store_coins(amount)
.store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_ref(body)
.end_cell();
send_raw_message(msg, 0);
}
Сама же игра начинается с проверки, что значание присланное в контракт равно 1 TON
:
() game::start(slice sender_addr, int msg_value, int hash) impure inline_ref {
throw_unless(exit::invalid_bet(), msg_value == 1ton());
}
Далее собирается хэш для рандома, собирается он из текущего времени, хэша из регистра с4
, хэша сформированного в recv_internal
из сообщения и cur_lt()
- логического времени текущей транзакции:
() game::start(slice sender_addr, int msg_value, int hash) impure inline_ref {
throw_unless(exit::invalid_bet(), msg_value == 1ton());
int new_hash = slice_hash(
begin_cell()
.store_uint(db::hash, 256)
.store_uint(hash, 256)
.store_uint(cur_lt(), 64)
.store_uint(now(), 64)
.end_cell()
.begin_parse()
);
}
С помощью хэша можно сформировать рандом, но прежде чем генерировать рандом с помощью функции rand()
рандомизируем хэш с помощью randomize.
() game::start(slice sender_addr, int msg_value, int hash) impure inline_ref { throw_unless(exit::invalid_bet(), msg_value == 1ton()); int new_hash = slice_hash( begin_cell() .store_uint(db::hash, 256) .store_uint(hash, 256) .store_uint(cur_lt(), 64) .store_uint(now(), 64) .end_cell() .begin_parse() );
randomize(new_hash);
}
Отмечу что это одна из возможных вариации реализации рандома, про рандом можно прочитать в документации - https://docs.ton.org/develop/smart-contracts/guidelines/random-number-generation
Для реализации вероятности выйгрыша генерируется число от 0 до 10000, здесь все просто, смотрим в какой перцентиль попало число и в зависимости от этого отправляем или не отпраaвляем выйгрышь:
() game::start(slice sender_addr, int msg_value, int hash) impure inline_ref {
throw_unless(exit::invalid_bet(), msg_value == 1ton());
int new_hash = slice_hash(
begin_cell()
.store_uint(db::hash, 256)
.store_uint(hash, 256)
.store_uint(cur_lt(), 64)
.store_uint(now(), 64)
.end_cell()
.begin_parse()
);
randomize(new_hash);
db::hash = new_hash;
int number = rand(10000); ;; [0; 10000)
db::last_number = number;
db::available_balance += 1ton();
if (number < 10) { ;; win 1/2 available balance
int win = db::available_balance / 2;
int commission = muldiv(win, 10, 100);
win -= commission;
db::available_balance -= (win + commission);
db::service_balance += commission;
game::payout(sender_addr, win, msg::jackpot());
return ();
}
if (number < 1000) { ;; win x5
int win = 5 * 1ton();
int commission = muldiv(win, 10, 100);
win -= commission;
db::available_balance -= (win + commission);
db::service_balance += commission;
game::payout(sender_addr, win, msg::x5());
return ();
}
if (number < 2000) { ;; win x2
int win = 2 * 1ton();
int commission = muldiv(win, 10, 100);
win -= commission;
db::available_balance -= (win + commission);
db::service_balance += commission;
game::payout(sender_addr, win, msg::x2());
return ();
}
}
Подобные туториалы и разборы по сети TON я пишу в свой канал - https://t.me/ton_learn . Буду рад вашей подписке.