This tutorial illustrates how to write a smart contract that has payable actions. Payable actions are actions that require you to transfer some tokens to actions prior to use other functionality of the smart contract. Also, the Antelope asset type is covered in this tutorial.
As for the logic of this smart contract, we're going to write a contract that accepts a particular token but will not allow the tokens to be withdrawn for a specific amount of time.
The token to HODL
First create a standard C++ class called "hodl" that extends eosio::contract.
In this multiple index table declaration, a new type called asset is used. An asset is a type designed to represent a digital token asset. See more details in the asset reference documentation.
The symbol member of an asset instance will be used as the primary key. By calling the raw() function the symbol variable will be converted into an unsigned integer so it can be used as a primary key.
To accept a transfer we need to have a deposit action.
[[eosio::on_notify("eosio.token::transfer")]]voiddeposit(name hodler,name to, eosio::asset quantity, std::string memo){if (to !=get_self() || hodler ==get_self()) {print("These are not the droids you are looking for.");return; }check(now() < the_party,"You're way late");check(quantity.amount >0,"When pigs fly");check(quantity.symbol == hodl_symbol,"These are not the droids you are looking for."); balance_table balance(get_self(),hodler.value);auto hodl_it =balance.find(hodl_symbol.raw());if (hodl_it !=balance.end())balance.modify(hodl_it,get_self(), [&](auto&row) {row.funds += quantity; });elsebalance.emplace(get_self(), [&](auto&row) {row.funds = quantity; });}
This action should not introduce many new concepts if you have followed this tutorial from the beginning.
Firstly, the action checks that the contract is not transferring to itself:
if (to !=get_self() || hodler ==get_self()) {print("These are not the droids you are looking for.");return;}
The contract needs to do so because transferring to the contract account itself would create an invalid booking situation in which an account could have more tokens than the account has in the eosio.token contract.
Then this action checks a few other conditions:
The time to withdraw has not already passed
The incoming transfer has a valid amount of tokens
The incoming transfer uses the token we specify in the constructor
check(now() < the_party,"You're way late");check(quantity.amount >0,"When pigs fly");check(quantity.symbol == hodl_symbol,"These are not the droids you are looking for.");
If all constraints are passed, the action updates the balances accordingly:
The important thing to note is the deposit function will actually be triggered by the eosio.token contract. To understand this behaviour we need to understand the on_notify attribute.
The on_notify attribute
[[eosio::on_notify("eosio.token::transfer")]]
The on_notify attribute is one of the EOSIO.CDT attributes that annotates a smart contract action.
Annotating an action with an on_notify attribute ensures any incoming notification is forwarded to the annotated action if and only if the notification is dispatched from a specified contract and from a specified action.
In this case, the on_notify attribute ensures the incoming notification is forward to the deposit action only if the notification comes from the eosio.token contract and is from the eosio.token's transfer action.
This is also why we don't need to check if the hodler actually has the appropriate amount of tokens he or she claimed, as the eosio.token contract would have done this check prior to the transfer notification reaching the hodldeposit action.
Party!
The party action will only allow withdrawals after the configured the_party time has elapsed. The party action has a similar construct as the deposit action with the following conditions:
check the withdrawing account is the account which made the deposit initially
find the locked balance
transfer the token on behalf of the account to the account itself
[[eosio::action]]voidparty(name hodler){ //Check the authority of hodlerrequire_auth(hodler); //Check the current time has passed the the_party timecheck(now() > the_party,"Hold your horses"); balance_table balance(get_self(),hodler.value);auto hodl_it =balance.find(hodl_symbol.raw()); //Make sure the holder is in the tablecheck(hodl_it !=balance.end(),"You're not allowed to party"); action{ permission_level{get_self(),"active"_n},"eosio.token"_n,"transfer"_n, std::make_tuple(get_self(), hodler,hodl_it->funds, std::string("Party! Your hodl is free.")) }.send();balance.erase(hodl_it);}
The complete code listing is the following:
#include<eosio/eosio.hpp>#include<eosio/print.hpp>#include<eosio/asset.hpp>#include<eosio/system.hpp>usingnamespace eosio;class[[eosio::contract("hodl")]]hodl:public eosio::contract {private:staticconstuint32_t the_party =1645525342;const symbol hodl_symbol;struct[[eosio::table]]balance { eosio::asset funds;uint64_tprimary_key() const { returnfunds.symbol.raw(); } };usingbalance_table= eosio::multi_index<"balance"_n,balance>;uint32_tnow() {returncurrent_time_point().sec_since_epoch(); }public:using contract::contract;hodl(name receiver,name code,datastream<constchar*> ds) :contract(receiver, code, ds),hodl_symbol("SYS",4){}[[eosio::on_notify("eosio.token::transfer")]]voiddeposit(name hodler,name to, eosio::asset quantity, std::string memo) {if (hodler ==get_self() || to !=get_self()) {return; }check(now() < the_party,"You're way late");check(quantity.amount >0,"When pigs fly");check(quantity.symbol == hodl_symbol,"These are not the droids you are looking for."); balance_table balance(get_self(),hodler.value);auto hodl_it =balance.find(hodl_symbol.raw());if (hodl_it !=balance.end())balance.modify(hodl_it,get_self(), [&](auto&row) {row.funds += quantity; });elsebalance.emplace(get_self(), [&](auto&row) {row.funds = quantity; }); }[[eosio::action]]voidparty(name hodler) { //Check the authority of hodlderrequire_auth(hodler); // //Check the current time has pass the the party timecheck(now() > the_party,"Hold your horses"); balance_table balance(get_self(),hodler.value);auto hodl_it =balance.find(hodl_symbol.raw()); // //Make sure the holder is in the tablecheck(hodl_it !=balance.end(),"You're not allowed to party"); action{ permission_level{get_self(),"active"_n},"eosio.token"_n,"transfer"_n, std::make_tuple(get_self(), hodler,hodl_it->funds, std::string("Party! Your hodl is free.")) }.send();balance.erase(hodl_it); }};
Finally, transfer some SYS tokens to the hodl contract from han's account.
infra-cli transfer han hodl '0.0001 SYS' 'Hodl!' -p han@active
Test withdraw
To test the withdrawal feature, the the_party variable needs to be updated. Update the the_party variable to a point in time in the past so the withdrawal functionality can be tested.
CONTRACT hodl : public eosio::contract {private: // 9 June 2018 01:00:00staticconstuint32_t the_party =1528549200;
Withdrawing the funds:
infra-cli push action hodl party '["han"]' -p han@active
Should produce the following response:
executed transaction: 62b1e6848c8c5e6458b9a0f7600e65574eaf60445be114d224adccc5a962a09a 104 bytes 383 us
# hodl <= hodl::party {"hodler":"han"}
# eosio.token <= eosio.token::transfer {"from":"hodl","to":"han","quantity":"0.0001 SYS","memo":"Party! Your hodl is free."}
# hodl <= eosio.token::transfer {"from":"hodl","to":"han","quantity":"0.0001 SYS","memo":"Party! Your hodl is free."}
# han <= eosio.token::transfer {"from":"hodl","to":"han","quantity":"0.0001 SYS","memo":"Party! Your hodl is free."}