HOW TO SCHEDULE AUTOMATIC SMART CONTRACT EXECUTION ON EOS

Emil G
mixbytes
Published in
4 min readJul 2, 2019

Introduction

Software development often involves tasks that should be performed at regular intervals. Unix systems, for instance, have a crond for these purposes.

Before we start talking about our solution, let’s first answer why we need cron in the blockchain.

There are quite a few applications, here are some of them:

  • You are a Dapp developer and store data in tables that take some RAM space. Memory costs money, so the application needs to periodically clean up these tables.
  • You are developing a currency converter and your application needs to periodically receive data from oracles.
  • You are a crypto fund manager and from time to time you need to send some profit share to customers.

We often have to audit and write smart contracts, and we regularly encounter the problems described above. Unfortunately, EOS does not allow to create periodic tasks, that’s why we tried to fill this gap by writing our cron smart contract (Cronos).

What is Cronos

Cronos is a cron smart contract for EOS that will help DApps perform periodic tasks.

Of course, you can manually call the contract methods or write a script that will do it for you. However, it is much easier and safer to pay a small fee for periodic cronos calls and stop worrying. Thus, you get some kind of cron-as-a-service :)

Working with Cronos (Memory cleanup)

The service works as follows: the user sends the required amount of EOS tokens to the smart contract. The smart contract then creates tasks using the schedule function. During task execution, a certain amount of EOS tokens will be automatically deducted from the contract.

For example, if you want to clean up smart contract storage every 42 seconds, you can create such task using Cronos:

cleos push action cron schedule '["andrew", "mydapp", "cleanup", 42]' -p andrew

Cleanup goes successfully if a developer correctly implements the cleanup action of the contract. An entry with the task and its execution time are created in Cronos table.

Under the hood

Cronos is pretty simple. The account owner once calls run with an interval for polling tables of the contract. Then, during contract execution, a deferred transaction with the same polling interval is created.

You can learn more about deferred transactions in the official documentation. Don’t forget to look up the use cases.

Mind that in EOS transaction execution time is limited to 30 ms. Therefore, it is reasonable to set the limit for the number of processed records in a single transaction (rows_count).

Here is the repository code with comments:

ACTION run(uint32_t polling_interval, uint32_t rows_count) {
// Make sure the transaction was created by the current contract
require_auth(get_self());

// Check whether execution should be stopped
if (stop_execution.get())
return;
// Record processing
scan_schedules(rows_count);
// Call run again in polling_interval seconds
create_transaction(_self, _self, "run", polling_interval, make_tuple(polling_interval, rows_count));
}
template<class ...TParams>
void create_transaction(name payer, name account, const string &action, uint32_t delay,
const std::tuple<TParams...>& args) {
// Create a deferred transaction with the required delay
eosio::transaction t;
t.actions.emplace_back(
permission_level(_code, "active"_n),
account,
name(action),
args);
t.delay_sec = delay;

// You will need a unique id for exception handling
auto sender_id = unique_id.get();
t.send(sender_id, payer);
unique_id.set(sender_id + 1, _code);
}

The logic of processing tasks in the table is quite simple. Cronos picks the task with the minimum execution time, ensures that it must be executed (i.e. the current time is less than or equal to the execution time), and creates a deferred transaction after making sure that the user has enough EOS tokens on the balance account. At the end of task processing, Cronos automatically recalculates the time of the next call.

// We get the unix-time of the next block creation  
time_point_sec current_time(now());
// We check whether it’s time to execute the task
if (current_time >= item.next_run) {
const name& account_from = item.from;
// Make sure the user has enough funds on his balance account
if (get_balance(account_from) >= CALL_PRICE) {
reduce_balance(account_from, CALL_PRICE);
create_transaction(account_from, item.account, item.action, item.period, tuple<name>(account_from));
// Refresh the runtime
cron_table.modify(item, _self, [&](auto& row) {
row.next_run = item.next_run + item.period;
});
}

Pitfalls of deferred transactions

The deferred transaction approach has a significant drawback. For instance, if the current transaction fails for some reason, a deferred transaction will not be created. To avoid this, you need to be careful with exception handling. Otherwise, the mydapp contract owner will have to call run action manually once again.

Next steps

Our team is going to add the functions that allow to set task rules similar to the original crond Unix ones.

References

Github repo:
https://github.com/mixbytes/cronos

Originally published at mixbytes.io.

--

--