From 82e908e853e0979ba366d9b636c4f9889177f94a Mon Sep 17 00:00:00 2001 From: Richard Holland Date: Tue, 7 Jun 2022 12:07:56 +0000 Subject: [PATCH] update peggy for v2 --- hook/examples/macro.h | 5 +- hook/examples/peggy/addtrustline.js | 31 +++ hook/examples/peggy/makefile | 5 + hook/examples/peggy/payusd.js | 33 +++ hook/examples/peggy/payxrp.js | 19 ++ hook/examples/peggy/peggy.c | 393 ++++++++++++++++++++++++++++ hook/examples/peggy/peggy.js | 35 +++ 7 files changed, 518 insertions(+), 3 deletions(-) create mode 100644 hook/examples/peggy/addtrustline.js create mode 100644 hook/examples/peggy/makefile create mode 100644 hook/examples/peggy/payusd.js create mode 100644 hook/examples/peggy/payxrp.js create mode 100644 hook/examples/peggy/peggy.c create mode 100644 hook/examples/peggy/peggy.js diff --git a/hook/examples/macro.h b/hook/examples/macro.h index 9a0e68fac..b0d142c7f 100644 --- a/hook/examples/macro.h +++ b/hook/examples/macro.h @@ -519,11 +519,10 @@ int out_len = 0;\ #else #define PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE 287 #endif -#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE(buf_out_master, tlamt, drops_fee_raw, to_address, dest_tag_raw, src_tag_raw)\ +#define PREPARE_PAYMENT_SIMPLE_TRUSTLINE(buf_out_master, tlamt, to_address, dest_tag_raw, src_tag_raw)\ {\ uint8_t* buf_out = buf_out_master;\ uint8_t acc[20];\ - uint64_t drops_fee = (drops_fee_raw);\ uint32_t dest_tag = (dest_tag_raw);\ uint32_t src_tag = (src_tag_raw);\ uint32_t cls = (uint32_t)ledger_seq();\ @@ -537,7 +536,7 @@ int out_len = 0;\ _02_27_ENCODE_LLS (buf_out, cls + 5 ); /* uint32 | size 6 */ \ _06_01_ENCODE_TL_AMOUNT (buf_out, tlamt ); /* amount | size 48 */ \ uint8_t* fee_ptr = buf_out;\ - _06_08_ENCODE_DROPS_FEE (buf_out, drops_fee ); /* amount | size 9 */ \ + _06_08_ENCODE_DROPS_FEE (buf_out, 0 ); /* amount | size 9 */ \ _07_03_ENCODE_SIGNING_PUBKEY_NULL (buf_out ); /* pk | size 35 */ \ _08_01_ENCODE_ACCOUNT_SRC (buf_out, acc ); /* account | size 22 */ \ _08_03_ENCODE_ACCOUNT_DST (buf_out, to_address ); /* account | size 22 */ \ diff --git a/hook/examples/peggy/addtrustline.js b/hook/examples/peggy/addtrustline.js new file mode 100644 index 000000000..acd7595c1 --- /dev/null +++ b/hook/examples/peggy/addtrustline.js @@ -0,0 +1,31 @@ +if (process.argv.length < 4) +{ + console.log("Usage: node addtrustline ") + process.exit(1) +} +const keypairs = require('ripple-keypairs'); +const secret = process.argv[2]; +const address = keypairs.deriveAddress(keypairs.deriveKeypair(secret).publicKey) +const hook_account = process.argv[3]; + +require('../../utils-tests.js').TestRig('ws://localhost:6005').then(t=> +{ + t.feeSubmit(secret, + { + Account: address, + TransactionType: "TrustSet", + Flags: 262144, + LimitAmount: { + currency: "USD", + issuer: hook_account, + value: "1000000000000000" + }, + }).then(x=> + { + console.log(x); + t.assertTxnSuccess(x); + process.exit(0); + }); +}); + + diff --git a/hook/examples/peggy/makefile b/hook/examples/peggy/makefile new file mode 100644 index 000000000..5ce354e55 --- /dev/null +++ b/hook/examples/peggy/makefile @@ -0,0 +1,5 @@ +all: + wasmcc peggy.c -o /tmp/peggy.wasm -O0 -Wl,--allow-undefined -I../ + wasm-opt -O2 /tmp/peggy.wasm -o peggy.wasm + hook-cleaner peggy.wasm + diff --git a/hook/examples/peggy/payusd.js b/hook/examples/peggy/payusd.js new file mode 100644 index 000000000..febe5f314 --- /dev/null +++ b/hook/examples/peggy/payusd.js @@ -0,0 +1,33 @@ +if (process.argv.length < 5) +{ + console.log("Usage: node payusd [destination] [dest tag]") + process.exit(1) +} +const keypairs = require('ripple-keypairs'); +const secret = process.argv[2]; +const address = keypairs.deriveAddress(keypairs.deriveKeypair(secret).publicKey) +const amount = process.argv[3] +const hook_account = process.argv[4]; + +const dest_acc = (process.argv.length >= 6 ? process.argv[5] : hook_account); +const dest_tag = (process.argv.length == 7 ? process.argv[6] : null); +require('../../utils-tests.js').TestRig('ws://localhost:6005').then(t=> +{ + t.feeSubmit(secret, + { + Account: address, + TransactionType: "Payment", + Amount: { + currency: "USD", + value: amount + '', + issuer: hook_account + }, + Destination: dest_acc + }).then(x=> + { + console.log(x); + process.exit(0); + }); +}); + + diff --git a/hook/examples/peggy/payxrp.js b/hook/examples/peggy/payxrp.js new file mode 100644 index 000000000..8165fcf28 --- /dev/null +++ b/hook/examples/peggy/payxrp.js @@ -0,0 +1,19 @@ +if (process.argv.length < 5) +{ + console.log("Usage: node pay ") + process.exit(1) +} +const secret = process.argv[2]; +const amount = BigInt(process.argv[3]) * 1000000n +const dest = process.argv[4]; + +require('../../utils-tests.js').TestRig('ws://localhost:6005').then(t=> +{ + t.pay(secret, amount, dest).then(x=> + { + console.log(x); + process.exit(0); + }); +}); + + diff --git a/hook/examples/peggy/peggy.c b/hook/examples/peggy/peggy.c new file mode 100644 index 000000000..9b269b56f --- /dev/null +++ b/hook/examples/peggy/peggy.c @@ -0,0 +1,393 @@ +/** + * Peggy.c - An oracle based stable coin hook + * + * Author: Richard Holland + * Date: 1 Mar 2021 + * + **/ + +#include +#include "../hookapi.h" + +// your vault starts at 150% collateralization +#define NEW_COLLATERALIZATION_NUMERATOR 2 +#define NEW_COLLATERALIZATION_DENOMINATOR 3 + +// at 120% collateralization your vault may be taken over +#define LIQ_COLLATERALIZATION_NUMERATOR 5 +#define LIQ_COLLATERALIZATION_DENOMINATOR 6 + + +// the oracle is the limit set on a trustline established between two special oracle accounts + +uint8_t oracle_lo[20] = { // require('ripple-address-codec').decodeAccountID('rXUMMaPpZqPutoRszR29jtC8amWq3APkx') + 0x05U, 0xb5U, 0xf4U, 0x3aU, 0xf7U, + 0x17U, 0xb8U, 0x19U, 0x48U, 0x49U, 0x1fU, 0xb7U, 0x07U, 0x9eU, 0x4fU, 0x17U, 0x3fU, 0x4eU, 0xceU, 0xb3U}; + +uint8_t oracle_hi[20] = { // require('ripple-address-codec').decodeAccountID('r9PfV3sQpKLWxccdg3HL2FXKxGW2orAcLE') + 0x5bU, 0xefU, 0x92U, 0x1aU, 0x21U, + 0x7dU, 0x57U, 0xfdU, 0xa5U, 0xb5U, 0x6dU, 0x5bU, 0x40U, 0xbeU, 0xe4U, 0x0dU, 0x1aU, 0xc1U, 0x12U, 0x7fU}; + +int64_t hook(uint32_t reserved) +{ + + etxn_reserve(1); + + uint8_t currency[20] = {0,0,0,0, 0,0,0,0, 0,0,0,0, 'U', 'S', 'D', 0,0,0,0,0}; + + // get the account the hook is running on and the account that created the txn + uint8_t hook_accid[20]; + hook_account(SBUF(hook_accid)); + + uint8_t otxn_accid[20]; + int32_t otxn_accid_len = otxn_field(SBUF(otxn_accid), sfAccount); + if (otxn_accid_len < 20) + rollback(SBUF("Peggy: sfAccount field missing!!!"), 10); + + // get the source tag if any... negative means it wasn't provided + int64_t source_tag = otxn_field(0,0, sfSourceTag); + if (source_tag < 0) + source_tag = 0xFFFFFFFFU; + + // compare the "From Account" (sfAccount) on the transaction with the account the hook is running on + int equal = 0; BUFFER_EQUAL(equal, hook_accid, otxn_accid, 20); + if (equal) + accept(SBUF("Peggy: Outgoing transaction"), 20); + + // invoice id if present is used for taking over undercollateralized vaults + // format: { 20 byte account id | 4 byte tag [FFFFFFFFU if absent] | 8 bytes of 0 } + uint8_t invoice_id[32]; + int64_t invoice_id_len = otxn_field(SBUF(invoice_id), sfInvoiceID); + + // check if a trustline exists between the sender and the hook for the USD currency [ PUSD ] + uint8_t keylet[34]; + if (util_keylet(SBUF(keylet), KEYLET_LINE, SBUF(hook_accid), SBUF(otxn_accid), SBUF(currency)) != 34) + rollback(SBUF("Peggy: Internal error, could not generate keylet"), 10); + + int64_t user_peggy_trustline_slot = slot_set(SBUF(keylet), 0); + TRACEVAR(user_peggy_trustline_slot); + if (user_peggy_trustline_slot < 0) + rollback(SBUF("Peggy: You must have a trustline set for USD to this account."), 10); + + + // because the trustline is actually a ripplestate object with a 'high' and a 'low' account + // we need to compare the hook account with the user's account to determine which side of the line to + // examine for an adequate limit + int compare_result = 0; + ACCOUNT_COMPARE(compare_result, hook_accid, otxn_accid); + if (compare_result == 0) + rollback(SBUF("Peggy: Invalid trustline set hi=lo?"), 1); + + int64_t lim_slot = slot_subfield(user_peggy_trustline_slot, (compare_result > 1 ? sfLowLimit : sfHighLimit), 0); + if (lim_slot < 0) + rollback(SBUF("Peggy: Could not find sfLowLimit on oracle trustline"), 20); + + int64_t user_trustline_limit = slot_float(lim_slot); + if (user_trustline_limit < 0) + rollback(SBUF("Peggy: Could not parse user trustline limit"), 1); + + int64_t required_limit = float_set(10, 1); + if (float_compare(user_trustline_limit, required_limit, COMPARE_EQUAL | COMPARE_GREATER) != 1) + rollback(SBUF("Peggy: You must set a trustline for USD to peggy for limit of at least 10B"), 1); + + // execution to here means the invoking account has the required trustline with the required limit + // now fetch the price oracle data (which also lives in a trustline) + + + CLEARBUF(keylet); + if (util_keylet(SBUF(keylet), KEYLET_LINE, SBUF(oracle_lo), SBUF(oracle_hi), SBUF(currency)) != 34) + rollback(SBUF("Peggy: Internal error, could not generate keylet"), 10); + + TRACEHEX(keylet); + int64_t slot_no = slot_set(SBUF(keylet), 0); + TRACEVAR(slot_no); + if (slot_no < 0) + rollback(SBUF("Peggy: Could not find oracle trustline"), 10); + + lim_slot = slot_subfield(slot_no, sfLowLimit, 0); + if (lim_slot < 0) + rollback(SBUF("Peggy: Could not find sfLowLimit on oracle trustline"), 20); + + int64_t exchange_rate = slot_float(lim_slot); + if (exchange_rate < 0) + rollback(SBUF("Peggy: Could not get exchange rate float"), 20); + + // execution to here means we have retrieved the exchange rate from the oracle + TRACEXFL(exchange_rate); + + // process the amount sent, which could be either xrp or pusd + // to do this we 'slot' the originating txn, that is: we place it into a slot so we can use the slot api + // to examine its internals + int64_t oslot = otxn_slot(0); + if (oslot < 0) + rollback(SBUF("Peggy: Could not slot originating txn."), 1); + + // specifically we're interested in the amount sent + int64_t amt_slot = slot_subfield(oslot, sfAmount, 0); + if (amt_slot < 0) + rollback(SBUF("Peggy: Could not slot otxn.sfAmount"), 2); + + int64_t amt = slot_float(amt_slot); + if (amt < 0) + rollback(SBUF("Peggy: Could not parse amount."), 1); + + // the slot_type api allows determination of fields and subtypes of fields according to the doco + // in this case we're examining an amount field to see if it's a native (xrp) amount or an iou amount + // this means passing flag=1 + int64_t is_xrp = slot_type(amt_slot, 1); + if (is_xrp < 0) + rollback(SBUF("Peggy: Could not determine sent amount type"), 3); + + + // In addition to determining the amount sent (and its type) we also need to handle the "recollateralization" + // takeover mode. This is where another user, not the original vault owner, passes the vault ID as invoice ID + // and sends a payment that brings the vault back into a valid state. We then assign ownership to this person + // as a reward for stablising the vault. So account for this and record whether or not we are proceeding as + // the original vault owner (or a new vault) or in takeover mode. + uint8_t is_vault_owner = 1; + uint8_t vault_key[24]; + if (invoice_id_len != 32) + { + // this is normal mode + for (int i = 0; GUARD(20), i < 20; ++i) + vault_key[i] = otxn_accid[i]; + + vault_key[20] = (uint8_t)((source_tag >> 24U) & 0xFFU); + vault_key[21] = (uint8_t)((source_tag >> 16U) & 0xFFU); + vault_key[22] = (uint8_t)((source_tag >> 8U) & 0xFFU); + vault_key[23] = (uint8_t)((source_tag >> 0U) & 0xFFU); + } + else + { + // this is the takeover mode + for (int i = 0; GUARD(24), i < 24; ++i) + vault_key[i] = invoice_id[i]; + is_vault_owner = 0; + } + + // check if state currently exists + uint8_t vault[16]; + int64_t vault_pusd = 0; + int64_t vault_xrp = 0; + uint8_t vault_exists = 0; + if (state(SBUF(vault), SBUF(vault_key)) == 16) + { + vault_pusd = float_sto_set(vault, 8); + vault_xrp = float_sto_set(vault + 8, 8); + vault_exists = 1; + } + else if (is_vault_owner == 0) + rollback(SBUF("Peggy: You cannot takeover a vault that does not exist!"), 1); + + if (is_xrp) + { + // XRP INCOMING + + // decide whether the vault is liquidatable + int64_t required_vault_xrp = float_divide(vault_pusd, exchange_rate); + required_vault_xrp = + float_mulratio(required_vault_xrp, 0, LIQ_COLLATERALIZATION_DENOMINATOR, LIQ_COLLATERALIZATION_NUMERATOR); + uint8_t can_liq = (required_vault_xrp < vault_xrp); + + // compute new vault xrp by adding the xrp they just sent + vault_xrp = float_sum(amt, vault_xrp); + + // compute the maximum amount of pusd that can be out according to the collateralization + int64_t max_vault_pusd = float_multiply(vault_xrp, exchange_rate); + max_vault_pusd = + float_mulratio(max_vault_pusd, 0, NEW_COLLATERALIZATION_NUMERATOR, NEW_COLLATERALIZATION_DENOMINATOR); + + // compute the amount we can send them + int64_t pusd_to_send = + float_sum(max_vault_pusd, float_negate(vault_pusd)); + if (pusd_to_send < 0) + rollback(SBUF("Peggy: Error computing pusd to send"), 1); + + // is the amount to send negative, that means the vault is undercollateralized + if (float_compare(pusd_to_send, 0, COMPARE_LESS)) + { + if (!is_vault_owner) + rollback(SBUF("Peggy: Vault is undercollateralized and your deposit would not redeem it."), 1); + else + { + if (float_sto(vault + 8, 8, 0,0,0,0, vault_xrp, -1) != 8) + rollback(SBUF("Peggy: Internal error writing vault"), 1); + if (state_set(SBUF(vault), SBUF(vault_key)) != 16) + rollback(SBUF("Peggy: Could not set state"), 1); + accept(SBUF("Peggy: Vault is undercollateralized, absorbing without sending anything."), 0); + } + } + + if (!is_vault_owner && !can_liq) + rollback(SBUF("Peggy: Vault is not sufficiently undercollateralized to take over yet."), 2); + + // execution to here means we will send out pusd + + // update the vault + vault_pusd = float_sum(vault_pusd, pusd_to_send); + + // if this is a takeover we destroy the vault on the old key and recreate it on the new key + if (!is_vault_owner) + { + // destroy + if (state_set(0,0,SBUF(vault_key)) < 0) + rollback(SBUF("Peggy: Could not destroy old vault."), 1); + + // reset the key + CLEARBUF(vault_key); + for (int i = 0; GUARD(20), i < 20; ++i) + vault_key[i] = otxn_accid[i]; + + vault_key[20] = (uint8_t)((source_tag >> 24U) & 0xFFU); + vault_key[21] = (uint8_t)((source_tag >> 16U) & 0xFFU); + vault_key[22] = (uint8_t)((source_tag >> 8U) & 0xFFU); + vault_key[23] = (uint8_t)((source_tag >> 0U) & 0xFFU); + } + + // set / update the vault + if (float_sto(vault, 8, 0,0,0,0, vault_pusd, -1) != 8 || + float_sto(vault + 8, 8, 0,0,0,0, vault_xrp, -1) != 8) + rollback(SBUF("Peggy: Internal error writing vault"), 1); + + if (state_set(SBUF(vault), SBUF(vault_key)) != 16) + rollback(SBUF("Peggy: Could not set state"), 1); + + // we need to dump the iou amount into a buffer + // by supplying -1 as the fieldcode we tell float_sto not to prefix an actual STO header on the field + uint8_t amt_out[48]; + if (float_sto(SBUF(amt_out), SBUF(currency), SBUF(hook_accid), pusd_to_send, -1) < 0) + rollback(SBUF("Peggy: Could not dump pusd amount into sto"), 1); + + // set the currency code and issuer in the amount field + for (int i = 0; GUARD(20),i < 20; ++i) + { + amt_out[i + 28] = hook_accid[i]; + amt_out[i + 8] = currency[i]; + } + + // finally create the outgoing txn + uint8_t txn_out[PREPARE_PAYMENT_SIMPLE_TRUSTLINE_SIZE]; + PREPARE_PAYMENT_SIMPLE_TRUSTLINE(txn_out, amt_out, otxn_accid, source_tag, source_tag); + + uint8_t emithash[32]; + if (emit(SBUF(emithash), SBUF(txn_out)) < 0) + rollback(SBUF("Peggy: Emitting txn failed"), 1); + + accept(SBUF("Peggy: Sent you PUSD!"), 0); + } + else + { + + // NON-XRP incoming + if (!vault_exists) + rollback(SBUF("Peggy: Can only send PUSD back to an existing vault."), 1); + + uint8_t amount_buffer[48]; + if (slot(SBUF(amount_buffer), amt_slot) != 48) + rollback(SBUF("Peggy: Could not dump sfAmount"), 1); + + // ensure the issuer is us + for (int i = 28; GUARD(20), i < 48; ++i) + { + if (amount_buffer[i] != hook_accid[i - 28]) + rollback(SBUF("Peggy: A currency we didn't issue was sent to us."), 1); + } + + // ensure the currency is PUSD + for (int i = 8; GUARD(20), i < 28; ++i) + { + if (amount_buffer[i] != currency[i - 8]) + rollback(SBUF("Peggy: A non USD currency was sent to us."), 1); + } + + TRACEVAR(vault_pusd); + + // decide whether the vault is liquidatable + int64_t required_vault_xrp = float_divide(vault_pusd, exchange_rate); + required_vault_xrp = + float_mulratio(required_vault_xrp, 0, LIQ_COLLATERALIZATION_DENOMINATOR, LIQ_COLLATERALIZATION_NUMERATOR); + uint8_t can_liq = (required_vault_xrp < vault_xrp); + + + // compute new vault pusd by adding the pusd they just sent + vault_pusd = float_sum(float_negate(amt), vault_pusd); + + // compute the maximum amount of pusd that can be out according to the collateralization + int64_t max_vault_xrp = float_divide(vault_pusd, exchange_rate); + max_vault_xrp = + float_mulratio(max_vault_xrp, 0, NEW_COLLATERALIZATION_DENOMINATOR, NEW_COLLATERALIZATION_NUMERATOR); + + + // compute the amount we can send them + int64_t xrp_to_send = + float_sum(float_negate(max_vault_xrp), vault_xrp); + + if (xrp_to_send < 0) + rollback(SBUF("Peggy: Error computing xrp to send"), 1); + + // is the amount to send negative, that means the vault is undercollateralized + if (float_compare(xrp_to_send, 0, COMPARE_LESS)) + { + if (!is_vault_owner) + rollback(SBUF("Peggy: Vault is undercollateralized and your deposit would not redeem it."), 1); + else + { + if (float_sto(vault, 8, 0,0,0,0, vault_pusd, -1) != 8) + rollback(SBUF("Peggy: Internal error writing vault"), 1); + + if (state_set(SBUF(vault), SBUF(vault_key)) != 16) + rollback(SBUF("Peggy: Could not set state"), 1); + + accept(SBUF("Peggy: Vault is undercollateralized, absorbing without sending anything."), 0); + } + } + + if (!is_vault_owner && !can_liq) + rollback(SBUF("Peggy: Vault is not sufficiently undercollateralized to take over yet."), 2); + + // execution to here means we will send out pusd + + // update the vault + vault_xrp = float_sum(vault_xrp, xrp_to_send); + + // if this is a takeover we destroy the vault on the old key and recreate it on the new key + if (!is_vault_owner) + { + // destroy + if (state_set(0,0,SBUF(vault_key)) < 0) + rollback(SBUF("Peggy: Could not destroy old vault."), 1); + + // reset the key + CLEARBUF(vault_key); + for (int i = 0; GUARD(20), i < 20; ++i) + vault_key[i] = otxn_accid[i]; + + vault_key[20] = (uint8_t)((source_tag >> 24U) & 0xFFU); + vault_key[21] = (uint8_t)((source_tag >> 16U) & 0xFFU); + vault_key[22] = (uint8_t)((source_tag >> 8U) & 0xFFU); + vault_key[23] = (uint8_t)((source_tag >> 0U) & 0xFFU); + } + + // set / update the vault + if (float_sto(vault, 8, 0,0,0,0, vault_pusd, -1) != 8 || + float_sto(vault + 8, 8, 0,0,0,0, max_vault_xrp, -1) != 8) + rollback(SBUF("Peggy: Internal error writing vault"), 1); + + if (state_set(SBUF(vault), SBUF(vault_key)) != 16) + rollback(SBUF("Peggy: Could not set state"), 1); + + // RH TODO: check the balance of the hook account + + + // finally create the outgoing txn + uint8_t txn_out[PREPARE_PAYMENT_SIMPLE_SIZE]; + PREPARE_PAYMENT_SIMPLE(txn_out, float_int(xrp_to_send, 6, 0), otxn_accid, source_tag, source_tag); + + uint8_t emithash[32]; + if (emit(SBUF(emithash), SBUF(txn_out)) < 0) + rollback(SBUF("Peggy: Emitting txn failed"), 1); + + accept(SBUF("Peggy: Sent you XRP!"), 0); + } + return 0; +} diff --git a/hook/examples/peggy/peggy.js b/hook/examples/peggy/peggy.js new file mode 100644 index 000000000..c4c6698a9 --- /dev/null +++ b/hook/examples/peggy/peggy.js @@ -0,0 +1,35 @@ +const wasm = 'peggy.wasm' +if (process.argv.length < 3) +{ + console.log("Usage: node peggy ") + process.exit(1); +} + + +require('../../utils-tests.js').TestRig('ws://localhost:6005').then(t=> +{ + const secret = process.argv[2]; + const account = t.xrpljs.Wallet.fromSeed(secret) + t.feeSubmit(process.argv[2], + { + Account: account.classicAddress, + TransactionType: "SetHook", + Hooks: [ + { + Hook: { + CreateCode: t.wasm(wasm), + HookApiVersion: 0, + HookNamespace: "CAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFECAFE", + HookOn: "0000000000000000", + Flags: t.hsfOVERRIDE + } + } + ] + }).then(x=> + { + t.assertTxnSuccess(x) + console.log(x); + process.exit(0); + + }).catch(t.err); +}).catch(e=>console.log(e));