Mix.install([
{:ethers, "~> 0.2.2"},
{:uniswap, github: "alisinabh/elixir_uniswap"},
:ex_secp256k1,
# For Images
:kino,
:ex_url
])
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"
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]
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]
# 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]
[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
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"
}
- Unsafe vs Safe math in Elixir
- Increasing Gas efficiency with a custom contract