Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Tutorial Exchange Deposit Withdraw

rwhitner edited this page May 14, 2018 · 10 revisions

Exchange Deposit and Withdrawal

This document is targeted toward exchanges that wish to automate deposit and withdrawal of standard-conforming EOSIO token contracts. The blockchain's native token conforms to the standard.

Configuring Nodeos

This tutorial uses the cleos command line tool to query a local nodeos server that should be connected to an eosio blockchain. nodeos will need to be configured with the following plugins:

  1. eosio::wallet_api_plugin
  2. eosio::history_api_plugin
  3. eosio::chain_api_plugin

By default, the history plugin will log the history of all accounts, but this is not the recommended configuration, as it will consume tens of gigabytes of RAM in the medium term. For a more optimized memory footprint, you should configure the history plugin to only log activity relevant to your account(s). This can be achieved with the following config param placed in your config.ini or passed on the command line.

$ nodeos --filter_on_accounts youraccount

Replaying the Blockchain

If you have already synced the blockchain without the history plugin, then you may need to replay the blockchain to pickup any historical activity.

$ nodeos --replay --filter_on_accounts youraccount 

You only need to replay once. Subsequent runs of nodeos should not use the replay flag, as your startup times will be unnecessarily long.

Accepting Deposits

When designing this tutorial, we assume that an exchange will poll nodeos for incoming transactions and will want to know when a transfer is considered irreversible or final.

With eosio-based chains, finality of a transaction occurs once 2/3 + 1 of block producers have either directly or indirectly confirmed the block. This could take from less than a second to a couple of minutes, but either way nodeos will keep you posted on the status.

Initial Condition

./cleos get currency balance eosio.token scott EOS
900.0000 EOS

We will now deposit some funds to exchange:

./cleos transfer scott exchange "1.0000 EOS" deposit
executed transaction: 5ec797175dd24612acd8fc5a8685fa44caa8646cec0a87b12568db22a3df02fb  256 bytes  8k cycles
#   eosio.token <= eosio.token::transfer        {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
>> transfer
#         scott <= eosio.token::transfer        {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
#      exchange <= eosio.token::transfer        {"from":"scott","to":"exchange","quantity":"1.0000 EOS","memo":""}
warning: transaction executed locally, but may not be confirmed by the network yet

This output indicates that the action eosio.token::transfer was delivered to three accounts/contracts, ("eosio.token", "scott", and "exchange"). The eosio token standard requires that both the sender and receiver account/contract be notified of all transfer actions so that those accounts can run custom logic. At this time, neither scott nor exchange has any contract set, but the transaction log still notes that they were notified.

Polling Account History

The account history consists of all actions that were either authorized by the account or received by the account. Since the exchange received the eosio.token::transfer action, it is listed in the history. If you are using the console, confirmed and irreversible transactions are printed in "green" while unconfirmed transactions are printed in "yellow". Without color, you can tell whether a transaction is confirmed or not by the first character: '#' for irreversible and '?' for potentially reversible.

./cleos get actions exchange
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================
#    0   2018-04-29T01:09:45.000     eosio.token::transfer => exchange      5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...

Do a few more transfers:

./cleos get actions exchange
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================
#    0   2018-04-29T01:09:45.000     eosio.token::transfer => exchange      5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
#    1   2018-04-29T01:16:25.000     eosio.token::transfer => exchange      2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
?    2   2018-04-29T01:19:54.000     eosio.token::transfer => exchange      213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...

The last transfer is still pending, waiting on irreversibility.

The "seq" column represents the index of actions for your specific account. It will always increment as new relevant actions are added.

The cleos get actions command allows you some control over which actions are fetched. You can view the help for this command with -h

./cleos get actions -h
Usage: ./cleos get actions [OPTIONS] account_name [pos] [offset]

Positionals:
  account_name TEXT           name of account to query on
  pos INT                     sequence number of action for this account, -1 for last
  offset INT                  get actions [pos,pos+offset] for positive offset or [pos-offset,pos) for negative offset

  Options:
    -j,--json                   print full json
    --full                      don't truncate action json
    --pretty                    pretty print full action json
    --console                   print console output generated by action

To get only the last action you would do the following:

./cleos get actions exchange -1 -1
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================
#    2   2018-04-29T01:19:54.000     eosio.token::transfer => exchange      213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...

This command says to go to the last sequence number (indicated by pos = -1) and then fetch "1" item prior to it (offset = -1). Using the example above, this will return a sequence in the range [3-1,3) or [2,3) which is only row 2. In this case, "-1" position means "one past the last sequence" and operates like an end iterator from C++ containers.

Fetching only "New" Actions

Since we presume your exchange is running a polling micro-service, it will want to fetch the "next unprocessed deposit". In this case the microservice will need to track the seq number of the "last processed seq". For the sake of this example, we will assume that "seq 0" has been processed and that we want to fetch "seq 1" if any.

We pass pos=1 and offset=0 to get the range [1,1+0] or [1,1].

./cleos get actions exchange 1 0
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================
#    1   2018-04-29T01:16:25.000     eosio.token::transfer => exchange      2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...

We can call this in a loop, processing each confirmed action (those starting with #) until we either run out of items or we find an unconfirmed action (starting with ?).

./cleos get actions exchange 3 0
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================

Machine Readable Account History (JSON)

So far this tutorial has focused on using cleos to fetch and display the history. However, cleos is merely a light-weight wrapper around a JSON-RPC interface. cleos can dump the raw json returned from the JSON-RPC request, or you can make your own JSON-RPC request.

Here is the JSON returned when querying sequence 2.

./cleos get actions exchange 2 0 -j
{
  "actions": [{
      "global_action_seq": 32856,
      "account_action_seq": 2,
      "block_num": 32759,
      "block_time": "2018-04-29T01:19:54.000",
      "action_trace": {
        "receipt": {
          "receiver": "exchange",
          "act_digest": "00686ff415fe97951a942889dbaed2b880043e3ae6ac2d5579318bbb2d30060f",
          "global_sequence": 32856,
          "recv_sequence": 3,
          "auth_sequence": [[
              "scott",
              43
            ]
          ]
        },
        "act": {
          "account": "eosio.token",
          "name": "transfer",
          "authorization": [{
              "actor": "scott",
              "permission": "active"
            }
          ],
          "data": {
            "from": "scott",
            "to": "exchange",
            "quantity": "1.0000 EOS",
            "memo": ""
          },
          "hex_data": "00000000809c29c20000008a4dd35057102700000000000004454f530000000000"
        },
        "elapsed": 52,
        "cpu_usage": 1000,
        "console": "",
        "total_inline_cpu_usage": 1000,
        "trx_id": "213f37972498cbae5abf6bcb5aec82e09967df7f04cf90f67b7d63a6bb871d58",
        "inline_traces": []
      }
    }
  ],
  "last_irreversible_block": 35062
}

Given this JSON, an action is irreversible (final) if "block_num" < "last_irreversible_block".

You can identify irreversible deposits by the following:

    actions[0].action_trace.act.account == "eosio.token" &&
    actions[0].action_trace.act.name == "transfer" &&
    actions[0].action_trace.act.data.quantity == "X.0000 EOS" &&
    actions[0].action_trace.to == "exchange" && 
    actions[0].action_trace.memo == "KEY TO IDENTIFY INTERNAL ACCOUNT" && 
    actions[0].action_trace.receipt.receiver == "exchange"  &&
    actions[0].block_num < last_irreversible_block

In practice you should give your customers a "memo" that identifies which of your internal accounts you should credit with the deposit.

WARNING

It is critical that you validate all of the conditions above, including the token symbol name. Users can create other contracts with "transfer" actions that "notify" your account. If you do not validate all of the above properties, then you may process "false deposits".

    actions[0].action_trace.act.account == "eosio.token" &&
    actions[0].action_trace.receipt.receiver == "exchange" 

Validating Balance

Now that we have received three deposits, we should see that the exchange has a balance of 3.0000 EOS.

./cleos get currency balance eosio.token exchange EOS
3.0000 EOS

Processing Withdrawals

(Note: while generating this tutorial, scott deposited another 1.0000 EOS (seq 3) for a total exchange balance of 4.0000 EOS.)

When a user requests a withdrawal from your exchange, they will need to provide you with their eosio account name and the amount to be withdrawn. You can then run the cleos command to perform the withdrawal. cleos will interact with the "unlocked" wallet running on nodeos, which should only enable localhost connections. More advanced usage will have a separate key-server (keosd), but that will be covered later.

Let's assume scott wants to withdraw 1.0000 EOS:

./cleos transfer exchange scott  "1.0000 EOS"
executed transaction: 93e785202e7502bb1383ad10e786cc20f7dd738d3fd3da38712b3fb38fb9af26  256 bytes  8k cycles
#   eosio.token <= eosio.token::transfer        {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
>> transfer
#      exchange <= eosio.token::transfer        {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
#         scott <= eosio.token::transfer        {"from":"exchange","to":"scott","quantity":"1.0000 EOS","memo":""}
warning: transaction executed locally, but may not be confirmed by the network yet

At this stage your local nodeos client accepted the transaction and likely broadcast it to the broader network.

We can get the history and see that there are "3" new actions listed, all with transaction id 93e78520..., as reported by our transfer command. Because exchange authorized the transaction, it is informed of all accounts that processed and accepted the transfer. In this case the eosio.token contract processed the transfer and updated balances, the sender (exchange) processed it, as did the receiver (scott). All three contracts/accounts approved it and/or performed state transitions based upon the action.

./cleos get actions exchange -1 -8
#  seq  when                              contract::action => receiver      trx id...   args
================================================================================================================
#    0   2018-04-29T01:09:45.000     eosio.token::transfer => exchange      5ec79717... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
#    1   2018-04-29T01:16:25.000     eosio.token::transfer => exchange      2269828c... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
#    2   2018-04-29T01:19:54.000     eosio.token::transfer => exchange      213f3797... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
#    3   2018-04-29T01:53:57.000     eosio.token::transfer => exchange      8b7766ac... {"from":"scott","to":"exchange","quantity":"1.0000 EOS","mem...
#    4   2018-04-29T01:54:17.500     eosio.token::transfer => eosio.token   93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...
#    5   2018-04-29T01:54:17.500     eosio.token::transfer => exchange      93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...
#    6   2018-04-29T01:54:17.500     eosio.token::transfer => scott         93e78520... {"from":"exchange","to":"scott","quantity":"1.0000 EOS","mem...

By processing the history, we can be informed when our transaction was confirmed. In practice, we expect you will maintain private database state to track the withdrawal process. It might be useful to embed an exchange-specific memo in the withdraw request in order to map to your private database state. Another approach is to simply use the transaction ID for your mapping. When your account history microservice comes across seq 5 and sees it is irreversible, it can then mark your withdrawal as complete.

Handling Errors

Sometimes network issues will cause a transaction to fail and never be included in a block. Your internal database will need to know when this has happened so that it can inform the user and/or try again. If you do not get an immediate error when you submit your local transfer, then you must wait for the transaction to expire. Every transaction has an "expiration", after which the transaction can never be applied. Once the last irreversible block has moved past the expiration time, you can safely mark your attempted withdrawal as failed and not worry about it "floating around the ether" to be applied when you least expect.

By default, cleos sets an expiration window of 2 minutes. This is long enough to allow all 21 producers an opportunity to include the transaction.

 ./cleos transfer exchange scott  "1.0000 EOS" -j -d
{
  "expiration": "2018-04-29T01:58:12",
  "ref_block_num": 37282,
  "ref_block_prefix": 351570603,
  "max_net_usage_words": 0,
  "max_kcpu_usage": 0,
  "delay_sec": 0,
  "context_free_actions": [],
  ...

Your microservice can query the last irreversible block number and the head block time using cleos.

./cleos get info
{
  "server_version": "0812f84d",
  "head_block_num": 39313,
  "last_irreversible_block_num": 39298,
  "last_irreversible_block_id": "000099823bfc4f0b936d8e48c70fc3f1619eb8d21989d160a9fe23655f1f5c79",
  "head_block_id": "000099912473a7a3699ad682f731d1874ebddcf4b60eff79f8e6e4216077278d",
  "head_block_time": "2018-04-29T02:14:31",
  "head_block_producer": "producer2"
}

Exchange Security

This tutorial shows the minimal viable deposit/withdraw handlers and assumes a single wallet which contains all keys necessary to authorize deposits and withdrawals. A security-focused exchange would take the following additional steps:

  1. keep vast majority of funds in a time-delayed, multi-sig controlled account
  2. use multi-sig on the hot wallet with several independent processes/servers double-checking all withdrawals
  3. deploy a custom contract that only allows withdrawals to KYC'd accounts and require multi-sig to white-list accounts
  4. deploy a custom contract that only accepts deposits of known tokens from KYC'd accounts
  5. deploy a custom contract that enforces a mandatory 24-hour waiting period for all withdrawals
  6. utilize hardware wallets for all signing, even automated withdrawal

Customers want immediate withdrawals, but they also want the exchange to be protected. The blockchain-enforced 24-hour period lets the customer know the money is "on the way" while also informing potential-hackers that the exchange has 24 hours to respond to unauthorized access. Furthermore, if the exchange emails/text messages users upon start of withdrawal, users have 24 hours to contact the exchange and fix any unauthorized access to their individual account.

Information on how to utilize these more advanced techniques will be available in a future document.