Skip to content

Instantly share code, notes, and snippets.

@alisinabh
Created January 23, 2024 18:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alisinabh/fe5f199c803b4679ce31d5e0e54aadb8 to your computer and use it in GitHub Desktop.
Save alisinabh/fe5f199c803b4679ce31d5e0e54aadb8 to your computer and use it in GitHub Desktop.
Elixir Ethers and Uniswap

Uniswap ❤️ Elixir

Mix.install([
  {:ethers, "~> 0.2.2"},
  {:uniswap, github: "alisinabh/elixir_uniswap"},
  :ex_secp256k1,
  # For Images
  :kino,
  :ex_url
])

Configuration

Setting up RPC and Signer data

# Testing on Goerli testnet
Application.put_env(:ethereumex, :url, "https://gateway.tenderly.co/public/goerli")
:ok
priv_key = System.get_env("LB_PRIVATE_KEY")
{:ok, [account]} = Ethers.Signer.Local.accounts(private_key: priv_key)

Application.put_env(:ethers, :default_signer, Ethers.Signer.Local)
Application.put_env(:ethers, :default_signer_opts, private_key: priv_key)

account
"0xC68aaEdc8295e8e2307bf4280e233a0Bb62056c9"

Minting Positions

First, pool data is required for creating the position.

usdc = "0xc2a9303EEF2ED1873adE85d2173D7AB6538971D5"
eurc = "0x12C8aAc4454b19CF9FE0918Ea56e6cc9d96bd829"

# 0.3%
pool =
  Uniswap.Contracts.V3Factory.get_pool(usdc, eurc, 3000)
  |> Ethers.call!()
"0xb45c5c5030765faf6c8a0a5c6f9c502689ca8f59"

tick_spacing is needed to calculate lower/upper tick bounds for creating a position.

It the steps which ticks are considered valid. (e.g. 10 means ticks that are divisable to 10 are valid)

tick_spacing =
  Uniswap.Contracts.V3Pool.tick_spacing()
  |> Ethers.call!(to: pool)
60

Calculate ticks using floating point prices (Unsafe Math)

lower_tick = Uniswap.Price.Utils.price_to_tick(0.90, tick_spacing, 6, 6)
upper_tick = Uniswap.Price.Utils.price_to_tick(1.25, tick_spacing, 6, 6)

{lower_tick, upper_tick}
{-1080, 2220}

View balances of our ERC20 tokens

calls = [
  {Ethers.Contracts.ERC20.balance_of(account), to: usdc},
  {Ethers.Contracts.ERC20.balance_of(account), to: eurc}
]

Ethers.Multicall.aggregate3(calls)
|> Ethers.call!()
|> Ethers.Multicall.decode(calls)
[true: 99998819456433, true: 99999777595699]

If not already done, we need to approve the NonfungiblePositionManager contract to spend our tokens using Ethers.Contracts.ERC20.approve/3.

Now we can mint a position using NonfungiblePositionManager.

# Mint tuple: {token_0, token_1, fee, tick_lower, tick_upper, amount_0, amount_1, 
#                min_amount_0, min_amount_1, recepient, deadline}

usdc_amount = trunc(100 * 1.09 * 10 ** 6)
eurc_amount = trunc(100 * 10 ** 6)

Uniswap.Contracts.NonfungiblePositionManager.mint(
  {eurc, usdc, 3000, lower_tick, upper_tick, usdc_amount, eurc_amount, 0, 0, account,
   1_000_000_000_000}
)

# |> Ethers.send!(from: account, max_fee_per_gas: 1297000, max_priority_fee_per_gas: 1297000)
#Ethers.TxData<
  function mint(
    (address,address,uint24,int24,int24,uint256,uint256,uint256,uint256,address,uint256) params {"0x12c8aac4454b19cf9fe0918ea56e6cc9d96bd829",
     "0xc2a9303eef2ed1873ade85d2173d7ab6538971d5", 3000, -1080, 2220, 109000000, 100000000, 0, 0,
     "0xc68aaedc8295e8e2307bf4280e233a0bb62056c9", 1000000000000}
  ) payable returns (
    uint256 tokenId,
    uint128 liquidity,
    uint256 amount0,
    uint256 amount1
  )
  default_address: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88"
>

Get the number of open positions that an account has.

Uniswap.Contracts.NonfungiblePositionManager.balance_of(account)
|> Ethers.call!()
3

Get a position token number (ERC721)

Uniswap.Contracts.NonfungiblePositionManager.token_of_owner_by_index(account, 2)
|> Ethers.call!()
91603

Get position data (Liquidity, Tick bounds, etc.)

Uniswap.Contracts.NonfungiblePositionManager.positions(91603)
|> Ethers.call!()
[0, "0x0000000000000000000000000000000000000000", "0x12c8aac4454b19cf9fe0918ea56e6cc9d96bd829",
 "0xc2a9303eef2ed1873ade85d2173d7ab6538971d5", 3000, -1080, 2220, 994503822,
 5251594197711366159301249239417349, 4095136457040771698219644190665784, 0, 0]

Collecting Fees

Bonus: See how much unclaimed fees a position has.

To collect the fees, just replace call with send

max = Ethers.Types.max({:uint, 128})

Uniswap.Contracts.NonfungiblePositionManager.collect({91603, account, max, max})
|> Ethers.call!(from: account, max_fee_per_gas: 1_297_000, max_priority_fee_per_gas: 1_297_000)
[0, 0]

Increasing/Decreasing Liquidity

# Increase Params: {token_id, amount_0, amount_1, min_amount_0, min_amount_1, deadline}

Uniswap.Contracts.NonfungiblePositionManager.increase_liquidity(
  {91603, 2_000_000, 2_000_000, 10, 10, 10_000_000_000_000}
)
|> Ethers.call!(from: account)
[21063762, 1356406, 2000000]
# Decrease Params: {token_id, liquidity_to_remove, min_amount_0, min_amount_1, deadline}

Uniswap.Contracts.NonfungiblePositionManager.decrease_liquidity(
  {91529, 10000, 10, 10, 10_000_000_000_000}
)
|> Ethers.call!(from: account)
[643, 949]

Getting Current price of the pool

[sqrt_price | _] =
  Uniswap.Contracts.V3Pool.slot0()
  |> Ethers.call!(to: pool)
[82586205569788209757413809326, 830, 0, 1, 1, 0, true]
Uniswap.Price.Utils.from_sqrt_x96(sqrt_price, 6, 6)
1.0865653669376387

NFT Position Image

url_data =
  Uniswap.Contracts.NonfungiblePositionManager.token_uri(91603)
  |> Ethers.call!()
  |> URL.new!()
  |> Map.get(:parsed_path)
  |> Map.get(:data)
  |> Jason.decode!()
  |> IO.inspect()

Kino.HTML.new("<img src=\"#{url_data["image"]}\"/>")
%{
  "description" => "This NFT represents a liquidity position in a Uniswap V3 USDC-EURC pool. The owner of this NFT can modify or redeem the position.\n\nPool Address: 0xb45c5c5030765faf6c8a0a5c6f9c502689ca8f59\nUSDC Address: 0xc2a9303eef2ed1873ade85d2173d7ab6538971d5\nEURC Address: 0x12c8aac4454b19cf9fe0918ea56e6cc9d96bd829\nFee Tier: 0.3%\nToken ID: 91603\n\n⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as token symbols may be imitated.",
  "image" => "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkwIiBoZWlnaHQ9IjUwMCIgdmlld0JveD0iMCAwIDI5MCA1MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnPjxkZWZzPjxmaWx0ZXIgaWQ9ImYxIj48ZmVJbWFnZSByZXN1bHQ9InAwIiB4bGluazpocmVmPSJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUIzYVdSMGFEMG5Namt3SnlCb1pXbG5hSFE5SnpVd01DY2dkbWxsZDBKdmVEMG5NQ0F3SURJNU1DQTFNREFuSUhodGJHNXpQU2RvZEhSd09pOHZkM2QzTG5jekxtOXlaeTh5TURBd0wzTjJaeWMrUEhKbFkzUWdkMmxrZEdnOUp6STVNSEI0SnlCb1pXbG5hSFE5SnpVd01IQjRKeUJtYVd4c1BTY2pZekpoT1RNd0p5OCtQQzl6ZG1jKyIvPjxmZUltYWdlIHJlc3VsdD0icDEiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwbk1qa3dKeUJvWldsbmFIUTlKelV3TUNjZ2RtbGxkMEp2ZUQwbk1DQXdJREk1TUNBMU1EQW5JSGh0Ykc1elBTZG9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5YytQR05wY21Oc1pTQmplRDBuTlRjbklHTjVQU2N5TWprbklISTlKekV5TUhCNEp5Qm1hV3hzUFNjak1USmpPR0ZoSnk4K1BDOXpkbWMrIi8+PGZlSW1hZ2UgcmVzdWx0PSJwMiIgeGxpbms6aHJlZj0iZGF0YTppbWFnZS9zdmcreG1sO2Jhc2U2NCxQSE4yWnlCM2FXUjBhRDBuTWprd0p5Qm9aV2xuYUhROUp6VXdNQ2NnZG1sbGQwSnZlRDBuTUNBd0lESTVNQ0ExTURBbklIaHRiRzV6UFNkb2RIUndPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1EQXdMM04yWnljK1BHTnBjbU5zWlNCamVEMG5NVEU0SnlCamVUMG5NemMxSnlCeVBTY3hNakJ3ZUNjZ1ptbHNiRDBuSXpnNU56RmtOU2N2UGp3dmMzWm5QZz09IiAvPjxmZUltYWdlIHJlc3VsdD0icDMiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwbk1qa3dKeUJvWldsbmFIUTlKelV3TUNjZ2RtbGxkMEp2ZUQwbk1DQXdJREk1TUNBMU1EQW5JSGh0Ykc1elBTZG9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5YytQR05wY21Oc1pTQmplRDBuTWpReEp5QmplVDBuTVRBM0p5QnlQU2N4TURCd2VDY2dabWxzYkQwbkl6WmlaRGd5T1NjdlBqd3ZjM1puUGc9PSIgLz48ZmVCbGVuZCBtb2RlPSJvdmVybGF5IiBpbj0icDAiIGluMj0icDEiIC8+PGZlQmxlbmQgbW9kZT0iZXhjbHVzaW9uIiBpbjI9InAyIiAvPjxmZUJsZW5kIG1vZGU9Im92ZXJsYXkiIGluMj0icDMiIHJlc3VsdD0iYmxlbmRPdXQiIC8+PGZlR2F1c3NpYW5CbHVyIGluPSJibGVuZE91dCIgc3RkRGV2aWF0aW9uPSI0MiIgLz48L2ZpbHRlcj4gPGNsaXBQYXRoIGlkPSJjb3JuZXJzIj48cmVjdCB3aWR0aD0iMjkwIiBoZWlnaHQ9IjUwMCIgcng9IjQyIiByeT0iNDIiIC8+PC9jbGlwUGF0aD48cGF0aCBpZD0idGV4dC1wYXRoLWEiIGQ9Ik00MCAxMiBIMjUwIEEyOCAyOCAwIDAgMSAyNzggNDAgVjQ2MCBBMjggMjggMCAwIDEgMjUwIDQ4OCBINDAgQTI4IDI4IDAgMCAxIDEyIDQ2MCBWNDAgQTI4IDI4IDAgMCAxIDQwIDEyIHoiIC8+PHBhdGggaWQ9Im1pbmltYXAiIGQ9Ik0yMzQgNDQ0QzIzNCA0NTcuOTQ5IDI0Mi4yMSA0NjMgMjUzIDQ2MyIgLz48ZmlsdGVyIGlkPSJ0b3AtcmVnaW9uLWJsdXIiPjxmZUdhdXNzaWFuQmx1ciBpbj0iU291cmNlR3JhcGhpYyIgc3RkRGV2aWF0aW9uPSIyNCIgLz48L2ZpbHRlcj48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQtdXAiIHgxPSIxIiB4Mj0iMCIgeTE9IjEiIHkyPSIwIj48c3RvcCBvZmZzZXQ9IjAuMCIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMSIgLz48c3RvcCBvZmZzZXQ9Ii45IiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIwIiAvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkLWRvd24iIHgxPSIwIiB4Mj0iMSIgeTE9IjAiIHkyPSIxIj48c3RvcCBvZmZzZXQ9IjAuMCIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMSIgLz48c3RvcCBvZmZzZXQ9IjAuOSIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMCIgLz48L2xpbmVhckdyYWRpZW50PjxtYXNrIGlkPSJmYWRlLXVwIiBtYXNrQ29udGVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCI+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVwKSIgLz48L21hc2s+PG1hc2sgaWQ9ImZhZGUtZG93biIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9InVybCgjZ3JhZC1kb3duKSIgLz48L21hc2s+PG1hc2sgaWQ9Im5vbmUiIG1hc2tDb250ZW50VW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ3aGl0ZSIgLz48L21hc2s+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXN5bWJvbCI+PHN0b3Agb2Zmc2V0PSIwLjciIHN0b3AtY29sb3I9IndoaXRlIiBzdG9wLW9wYWNpdHk9IjEiIC8+PHN0b3Agb2Zmc2V0PSIuOTUiIHN0b3AtY29sb3I9IndoaXRlIiBzdG9wLW9wYWNpdHk9IjAiIC8+PC9saW5lYXJHcmFkaWVudD48bWFzayBpZD0iZmFkZS1zeW1ib2wiIG1hc2tDb250ZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48cmVjdCB3aWR0aD0iMjkwcHgiIGhlaWdodD0iMjAwcHgiIGZpbGw9InVybCgjZ3JhZC1zeW1ib2wpIiAvPjwvbWFzaz48L2RlZnM+PGcgY2xpcC1wYXRoPSJ1cmwoI2Nvcm5lcnMpIj48cmVjdCBmaWxsPSJjMmE5MzAiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjkwcHgiIGhlaWdodD0iNTAwcHgiIC8+PHJlY3Qgc3R5bGU9ImZpbHRlcjogdXJsKCNmMSkiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjkwcHgiIGhlaWdodD0iNTAwcHgiIC8+IDxnIHN0eWxlPSJmaWx0ZXI6dXJsKCN0b3AtcmVnaW9uLWJsdXIpOyB0cmFuc2Zvcm06c2NhbGUoMS41KTsgdHJhbnNmb3JtLW9yaWdpbjpjZW50ZXIgdG9wOyI+PHJlY3QgZmlsbD0ibm9uZSIgeD0iMHB4IiB5PSIwcH" <> ...,
  "name" => "Uniswap - 0.3% - USDC/EURC - 0.89763<>1.2486"
}

Final Words

  • Unsafe vs Safe math in Elixir
  • Increasing Gas efficiency with a custom contract
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment