diff --git a/movevm/liquidity-provider/Move.toml b/movevm/liquidity-provider/Move.toml new file mode 100644 index 00000000..6e0bff07 --- /dev/null +++ b/movevm/liquidity-provider/Move.toml @@ -0,0 +1,26 @@ +[package] +name = "liquidity-provider" +version = "1.0.0" +authors = [] + + +[addresses] +# Do not change +initia_std = "0x1" +std = "0x1" + +# Address of uinit:uusdc pool +pair = "0xdbf06c48af3984ec6d9ae8a9aa7dbb0bb1e784aa9b8c4a5681af660cf8558d7d" + +# Address of uinit token +asset = "0x8e4733bdabcf7d4afc3d14f0dd46c9bf52fb0fce9e4b996c939e195b8bc891d9" + +# Contract owner +me = "_" + +# Receives LP tokens +recipient = "_" + + +[dependencies] +InitiaStdlib = { git = "https://github.com/initia-labs/movevm", subdir = "precompile/modules/initia_stdlib", rev = "main" } diff --git a/movevm/liquidity-provider/README.md b/movevm/liquidity-provider/README.md new file mode 100644 index 00000000..22e718c7 --- /dev/null +++ b/movevm/liquidity-provider/README.md @@ -0,0 +1,71 @@ +# Initia MoveVM "provide liquidity and send LP" module + +#### 1. Prepare an empty address with INIT tokens + +You might need to use a [faucet](https://faucet.testnet.initia.xyz/) for that. +This way, you should have: + +- initiad binary; +- mnemonic with some INIT tokens on initiation-2 network. + +#### 2. Configure deployment + +Navigate to `Move.toml` and open it in your editor of choice. You are interested in the +section `[addresses]`. `me` and `recipient` are filled with placeholder (`_`) addresses, +so you will have to fill them up. You can use this easy snippet to generate hexadecimal +addresses from keys stored in your initiad keychain: + +```bash +NAME=""; echo "0x$(initiad keys parse "$(initiad keys show "$NAME" --output json | jq -r '.address')" --output json | jq -r '.bytes' | tr '[:upper:]' '[:lower:]')" +``` + +First, fill `me` to the address of your own account. For `recipient`, you can create a new +empty account and use it's address. + +#### 3. Build module + +It is as easy as `initiad move build`. + +#### 4. Deploy module + +It is also easy, sign your normal Cosmos SDK transaction: + +```bash +initiad move deploy --path "$(pwd)" --upgrade-policy COMPATIBLE --from --gas auto --gas-adjustment 1.5 --gas-prices 0.025uinit --node https://rpc.initiation-2.initia.xyz:443 --chain-id initiation-2 +``` + +#### 5. Determine address of module object + +Open module upload transaction in block explorer, for example take a look at +[this one](https://scan.testnet.initia.xyz/initiation-1/txs/7B408B00337E840D0AF2BB89615CEEFFD73A28458D1BD31185418909FAF37BDB). +Look for event log with `type_tag` equal to `0x1::object::CreateEvent`. +Inside this event there is a JSON, containing a field `object` with the +address of our new module object. This address is where INIT tokens are expected +to be deposited to. + +#### 6. Send INIT tokens to the module object + +Should be as easy as a normal Cosmos SDK bank transfer, with a single caveat. +You have object address in HEX format, but you need bech32. Let's convert it: +suppose you have address `0x8a6fc188562db0e6008896b4e7a5ec027fe3461cb4169adc5165d1b58732d720`. +Run `initiad keys parse 8a6fc188562db0e6008896b4e7a5ec027fe3461cb4169adc5165d1b58732d720`, +take the first output with `init1` prefix: `init13fhurzzk9kcwvqygj66w0f0vqfl7x3sukstf4hz3vhgmtpej6usqzqa0mq`, +this would be the address to send funds to: + +```bash +initiad tx bank send init13fhurzzk9kcwvqygj66w0f0vqfl7x3sukstf4hz3vhgmtpej6usqzqa0mq 4242uinit --gas auto --gas-adjustment 1.5 --gas-prices 0.025uinit --chain-id initiation-2 --node https://rpc.initiation-2.initia.xyz:443 +``` + +#### 7. Execute contract + +```bash +initiad tx move execute
liquidity_provider provide --from testnet --gas auto --gas-adjustment 1.5 --gas-prices 0.025uinit --node https://rpc.initiation-2.initia.xyz:443 --chain-id initiation-2 +``` + +#### 8. Validate + +Use block explorer to validate that: + +- @me address doesn't have any INIT tokens anymore; +- @recipient address has some LP tokens (denom is + `move/dbf06c48af3984ec6d9ae8a9aa7dbb0bb1e784aa9b8c4a5681af660cf8558d7d`). diff --git a/movevm/liquidity-provider/all_pairs.bash b/movevm/liquidity-provider/all_pairs.bash new file mode 100755 index 00000000..9c568d92 --- /dev/null +++ b/movevm/liquidity-provider/all_pairs.bash @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Fetch all DEX pairs on initiation-2 and print their info + +set -euo pipefail +IFS=$'\n\t' + +declare -a q=( + "--output" "json" + "--node" "https://rpc.initiation-2.initia.xyz:443" +) + +all="$(initiad query move view 0x1 dex get_all_pairs --args '["option
:null", "option
:null", "option
:null", "u8:255"]' "${q[@]}" | jq -r '.data')" +for pair in $(echo "$all" | jq -rc '.[]'); do + lp="$(echo "$pair" | jq -r '.liquidity_token')" + coin_a="$(echo "$pair" | jq -r '.coin_a')" + coin_b="$(echo "$pair" | jq -r '.coin_b')" + metadata="$(initiad query move resource "$lp" 0x1::fungible_asset::Metadata "${q[@]}")" + name="$(echo "$metadata" | jq -r '.resource.move_resource' | jq '.data.name')" + symbol="$(echo "$metadata" | jq -r '.resource.move_resource' | jq '.data.symbol')" + supply="$(initiad query move resource "$lp" 0x1::fungible_asset::Supply "${q[@]}")" + supply="$(echo "$supply" | jq -r '.resource.move_resource' | jq '.data.current')" + echo "LP: $lp, name: $name, symbol: $symbol, supply: $supply, coin_a: $coin_a, coin_b: $coin_b" +done diff --git a/movevm/liquidity-provider/sources/poc.move b/movevm/liquidity-provider/sources/poc.move new file mode 100644 index 00000000..9432962d --- /dev/null +++ b/movevm/liquidity-provider/sources/poc.move @@ -0,0 +1,149 @@ +module me::liquidity_provider { + use initia_std::dex; + use initia_std::object::{Self, Object, ExtendRef}; + use initia_std::coin; + use initia_std::json; + use initia_std::cosmos; + use initia_std::oracle; + use initia_std::bigdecimal; + use initia_std::math128; + use initia_std::block; + use initia_std::fungible_asset::Metadata; + use std::option; + use std::signer; + use std::address; + use std::string::{Self, String}; + use std::error; + + const ENOT_OWNER: u64 = 1; + + struct ModuleStore has key { + extend_ref: ExtendRef, + price: u256, + ts: u64, + decimals: u64, + } + + struct MsgExecuteJSON has drop { + _type_: String, + sender: String, + module_address: String, + module_name: String, + function_name: String, + type_args: vector, + args: vector, + } + + fun init_module(creator: &signer) { + let constructor_ref = object::create_object(@me, false); + let extend_ref = object::generate_extend_ref(&constructor_ref); + move_to(creator, ModuleStore { extend_ref, price: 0, ts: 0, decimals: 0, }); + } + + // emits stargate message which: + // 1. calls @me::liquidity_provider::store() + // 2.1. then calls @me::liquidity_provider(1, false) if store() fails + // 2.2. then calls @me::liquidity_provider(1, true) if store() succeeds + public entry fun provide() acquires ModuleStore { + let store = borrow_global(@me); + let signer = object::generate_signer_for_extending(&store.extend_ref); + let addr = signer::address_of(&signer); + + let msg = MsgExecuteJSON { + _type_: string::utf8(b"/initia.move.v1.MsgExecuteJSON"), + sender: address::to_sdk(addr), + module_address: address::to_sdk(@me), + module_name: string::utf8(b"liquidity_provider"), + function_name: string::utf8(b"store"), + type_args: vector[], + args: vector[], + }; + + let fid = address::to_string(@me); + string::append(&mut fid, string::utf8(b"::liquidity_provider::callback")); + + cosmos::stargate_with_options( + &signer, + json::marshal(&msg), + cosmos::allow_failure_with_callback(1, fid), + ); + } + + // Last resort function only available for module admin to withdraw all funds of `coin` denomination + // from module's object address in case if there is an unrecoverable bug somewhere + public entry fun backup(account: &signer, coin: Object) acquires ModuleStore { + assert!( + signer::address_of(account) == @me, + error::permission_denied(ENOT_OWNER), + ); + + let store = borrow_global(@me); + let ref_signer = object::generate_signer_for_extending(&store.extend_ref); + let balance = coin::balance(signer::address_of(&ref_signer), coin); + coin::transfer(&ref_signer, signer::address_of(account), coin, balance); + } + + // 1. provide liquidity + // 2. sweep all received LP tokens to @recipient + fun provide_liquidity(account: &signer) { + let addr = signer::address_of(account); + + let metadata_in = object::address_to_object(@asset); + let amount_in = coin::balance(addr, metadata_in); + dex::single_asset_provide_liquidity_script( + account, + object::address_to_object(@pair), + metadata_in, + amount_in, + option::none(), + ); + + let metadata_out = object::address_to_object(@pair); + let amount_out = coin::balance(addr, metadata_out); + coin::transfer(account, @recipient, metadata_out, amount_out); + } + + // Read INIT price from slinky + entry fun store() acquires ModuleStore { + let (price, ts, decimals) = oracle::get_price(string::utf8(b"INIT/USD")); + let store = borrow_global_mut(@me); + store.price = price; + store.ts = ts; + store.decimals = decimals; + } + + // MEV protection, only provide liquidity if pool price is up to date with off-chain price feed + entry fun callback(_id: u64, success: bool) acquires ModuleStore { + let store = borrow_global(@me); + let signer = object::generate_signer_for_extending(&store.extend_ref); + + if (success) { + let slinky_price = bigdecimal::from_ratio_u256( + store.price, + (math128::pow(10, (store.decimals as u128)) as u256), + ); + let pool_price = dex::get_spot_price( + object::address_to_object(@pair), + object::address_to_object(@asset), + ); + let ratio = if (bigdecimal::gt(slinky_price, pool_price)) { + bigdecimal::div(slinky_price, pool_price) + } else { + bigdecimal::div(pool_price, slinky_price) + }; + let block_ts = block::get_current_block_timestamp(); + if ( + // slinky price is up to date + store.ts == block_ts + && + // pool price is not more than 1% off + bigdecimal::le(ratio, bigdecimal::from_ratio_u256(101, 1)) + ) { + provide_liquidity(&signer); + } + } else { + // slinky doesn't know about INIT yet, skip any validation and go full YOLO + provide_liquidity(&signer); + } + } +}