From 1ce248a7330d90e4a113e40e9452cea9e337ad32 Mon Sep 17 00:00:00 2001 From: David Hintermann <David.Hintermann@ost.ch> Date: Sun, 1 Dec 2024 14:53:42 +0000 Subject: [PATCH] wip: rebalance pool, rebalance reserves --- .env.sample | 7 +- .../Interfaces/IPotatoMarketplace.cs | 3 +- .../Marketplaces/PotatoMarketplaceMock.cs | 13 +- Frontend/Program.cs | 3 + Frontend/PrometherusClient.cs | 45 +++++ Frontend/Rebalancer.cs | 158 ++++++++++++++++++ 6 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 Frontend/PrometherusClient.cs create mode 100644 Frontend/Rebalancer.cs diff --git a/.env.sample b/.env.sample index 03ce735..8d2204f 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,7 @@ API_URL=https://sepolia.infura.io/v3/{yourApiKey} -ACCOUNT_PRIVATE_KEY= \ No newline at end of file +ACCOUNT_PRIVATE_KEY= +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=roesticoin +POSTGRES_USER=roesticoin +PROMETHEUS_BASE_URL=http://prometheus:9000 \ No newline at end of file diff --git a/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs b/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs index 9be9856..11e4b22 100644 --- a/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs +++ b/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs @@ -9,8 +9,9 @@ namespace Frontend.Marketplaces.Interfaces public Task<double> EthPriceInChf { get; } public Task<double> PotatoKgPriceInChf { get; } public Task<double> PotatoKgPriceInEth { get; } - public Task<bool> Approve(Web3 approverClient, BigInteger amount); public Task<PotatoTransaction?> Buy(string ownerAddress); public Task<bool> Sell(PotatoTransaction transaction, string receiverAddress); + + string Address { get; } } } diff --git a/Frontend/Marketplaces/PotatoMarketplaceMock.cs b/Frontend/Marketplaces/PotatoMarketplaceMock.cs index 2d2024a..1e638aa 100644 --- a/Frontend/Marketplaces/PotatoMarketplaceMock.cs +++ b/Frontend/Marketplaces/PotatoMarketplaceMock.cs @@ -24,18 +24,7 @@ public class PotatoMarketplaceMock( public Task<double> PotatoKgPriceInChf => Task.FromResult(potatoKgChfPrice); public Task<double> PotatoKgPriceInEth => PotatoKgEthPrice(); - public async Task<bool> Approve(Web3 approverClient, BigInteger amountInWei) - { - var approveHandler = approverClient.Eth.GetContractTransactionHandler<ApproveFunction>(); - var approveFunction = new ApproveFunction - { - Spender = account.Address, - Value = amountInWei, - }; - - var receipt = await approveHandler.SendRequestAndWaitForReceiptAsync(wethAddress, approveFunction); - return receipt.Status.Value == 1; - } + public string Address => account.Address; public async Task<PotatoTransaction?> Buy(string ownerAddress) { diff --git a/Frontend/Program.cs b/Frontend/Program.cs index 142bd0d..bf116b6 100644 --- a/Frontend/Program.cs +++ b/Frontend/Program.cs @@ -26,6 +26,7 @@ var postgresHost = Environment.GetEnvironmentVariable("POSTGRES_HOST")!; var postgresUser = Environment.GetEnvironmentVariable("POSTGRES_USER")!; var postgresPassword = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")!; var postgresDb = Environment.GetEnvironmentVariable("POSTGRES_DB")!; +var prometheusBaseUrl =Environment.GetEnvironmentVariable("PROMETHEUS_BASE_URL")!; var builder = WebApplication.CreateBuilder(args); @@ -61,6 +62,8 @@ builder.Services.AddTransient<Web3>(sp => return new Web3(account, chainApiUrl); }); +builder.Services.AddTransient<PrometheusClient>(sp => new PrometheusClient(prometheusBaseUrl)); + builder.Services.AddSingleton<PoolMetrics>(); builder.Services.AddSingleton<IPotatoMarketPlace>(sp => { diff --git a/Frontend/PrometherusClient.cs b/Frontend/PrometherusClient.cs new file mode 100644 index 0000000..cba3930 --- /dev/null +++ b/Frontend/PrometherusClient.cs @@ -0,0 +1,45 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Frontend; +public class PrometheusClient +{ + private readonly HttpClient _httpClient; + private readonly string _prometheusBaseUrl; + + public PrometheusClient(string prometheusBaseUrl) + { + _httpClient = new HttpClient(); + _prometheusBaseUrl = prometheusBaseUrl; + } + + public async Task<double> QueryMetricAsync(string metricName) + { + var queryUrl = $"{_prometheusBaseUrl}/api/v1/query?query={metricName}"; + var response = await _httpClient.GetAsync(queryUrl); + + if (!response.IsSuccessStatusCode) + { + throw new Exception($"Error querying Prometheus: {response.ReasonPhrase}"); + } + + var content = await response.Content.ReadAsStringAsync(); + var json = JObject.Parse(content); + + if (json["status"]!.ToString() != "success") + { + throw new Exception($"Error querying Prometheus: {json["error"]}"); + } + + var result = json["data"]!["result"]!; + if (result.HasValues) + { + var value = result[0]!["value"]![1]!.ToString(); + return double.Parse(value); + } + + throw new Exception("Metric not found"); + } +} \ No newline at end of file diff --git a/Frontend/Rebalancer.cs b/Frontend/Rebalancer.cs new file mode 100644 index 0000000..99ec0d1 --- /dev/null +++ b/Frontend/Rebalancer.cs @@ -0,0 +1,158 @@ +using System.Numerics; +using System.Reflection; +using Frontend.Assets; +using Frontend.AssetStores; +using Frontend.Configuration; +using Frontend.Functions; +using Frontend.Marketplaces; +using Frontend.Marketplaces.Interfaces; +using Nethereum.Contracts; +using Nethereum.Contracts.Standards.ERC20.ContractDefinition; +using Nethereum.Web3; +using Nethereum.Web3.Accounts; +using Org.BouncyCastle.Crypto.Tls; + +namespace Frontend; + +public class Rebalancer +{ + const decimal EPSIOLON = 0.001m; + private readonly Web3 _web3; + private readonly Account _account; + private readonly string _contractAddress; + private readonly PrometheusClient _prometheusClient; + private decimal _threshold = 0.01m; + private PoolV3Client _pool; + private readonly IPotatoMarketPlace _potatoMarket; + private readonly ILogger<Rebalancer> _logger; + public uint RebalanceTradeExecutedCount { get; private set; } + private readonly TokenClient _token0Client; + private readonly TokenClient _token1Client; + private readonly PotatoStorage _potatoStorage; + + public Rebalancer( + Web3 web3, + PrometheusClient prometheusClient, + ChainSettings chainSettings, + Account account, + IPotatoMarketPlace potatoMarket, + ILogger<Rebalancer> logger, + PotatoStorage potatoStorage) + { + _web3 = web3; + _prometheusClient = prometheusClient; + _pool = new(web3, chainSettings, account); + _potatoMarket = potatoMarket; + _logger = logger; + RebalanceTradeExecutedCount = 0; + _token0Client = new TokenClient(_web3, _pool.Token0Address); + _token1Client = new TokenClient(_web3, _pool.Token1Address); + _potatoStorage = potatoStorage; + } + + + public async Task<decimal> GetPoolPriceAsync() + { + return (decimal)await _prometheusClient.QueryMetricAsync("Uniswap_ROCWETH_price_WETH_per_ROC"); + + } + + public async Task<decimal> GetDesiredPoolPriceAsync() + { + return (decimal)await _prometheusClient.QueryMetricAsync("avg_over_time(Marketplace_Potato_Price_ETH[2m])"); + } + + public async Task MaintainPoolAsync() + { + var currentPrice=0.0m; + var desiredPrice=0.0m; + try{ + currentPrice = await GetPoolPriceAsync(); + desiredPrice = await GetDesiredPoolPriceAsync(); + }catch(Exception e){ + _logger.LogError("Error while getting prices: {}", e.Message); + return; + } + if (Math.Abs(1 - currentPrice / desiredPrice) > _threshold) + { + _logger.LogDebug("Must rebalance: Mean price {}, desired price {}", currentPrice, desiredPrice); + var quoteAfterSwap = await _pool.PerformSwapTowardsDesiredQuote(desiredPrice); + _logger.LogDebug("Quote afert swap: {}", quoteAfterSwap); + RebalanceTradeExecutedCount++; + } + } + + public async Task MaintainLiquidReservesAsync() + { + var token0WeiReserve = await _token0Client.BalanceOf(_account.Address); + var token1WeiReserve = await _token1Client.BalanceOf(_account.Address); + var meanPoolPrice = await GetPoolPriceAsync(); + var token0ReservesInToken1 = Web3.Convert.FromWei(token0WeiReserve) * meanPoolPrice; + var token1Reserve = Web3.Convert.FromWei(token1WeiReserve); + if (token0ReservesInToken1 / token1Reserve > 2) // token0 is ROC , token1 is WETH, our reserves in ROC are twice as big as in WETH + { + _logger.LogDebug("Must rebalance reserves: Token0 reserves {}(expressed in token1), Token1 reserves {}(expressed in token1)", token0ReservesInToken1, token1Reserve); + var amountToAdjustInToken1 = (token0ReservesInToken1 - token1Reserve) / 2; + var amountToAdjustInToken0 = amountToAdjustInToken1 / meanPoolPrice; + _logger.LogDebug("Half of the difference is {}(expressed in token0) or {}(expressed in token1). Sell it", amountToAdjustInToken0,amountToAdjustInToken1); + var transactionToSell= await _potatoStorage.RemovePotatoes((double)amountToAdjustInToken0); + var result= await _potatoMarket.Sell(transactionToSell, _account.Address); + if(result){ + _logger.LogDebug("Potatoes sold successfully, burn the torken representation"); + await _token0Client.Burn(Web3.Convert.ToWei(transactionToSell.Weight), new CancellationToken()); + }else{ + _logger.LogError("Potatoes not sold successfully, keep the token representation"); + } + }else if(token1Reserve/token0ReservesInToken1 >2){ + _logger.LogDebug("Must rebalance reserves: Token0 reserves {}(expressed in token1), Token1 reserves {}(expressed in token1)", token0ReservesInToken1, token1Reserve); + var amountToAdjustInToken1 = (token1Reserve - token0ReservesInToken1) / 2; + var amountToAdjustInToken0 = amountToAdjustInToken1 / meanPoolPrice; + _logger.LogDebug("Half of the difference is {}(expressed in token0) or {}(expressed in token1). Buy it", amountToAdjustInToken0,amountToAdjustInToken1); + await _token1Client.ApproveAsync(_potatoMarket.Address,Web3.Convert.ToWei(amountToAdjustInToken1)); + var buyTransaction= await _potatoMarket.Buy(_account.Address); + if(buyTransaction!=null){ + _logger.LogDebug("Potatoes bought successfully, mint the token representation"); + await _potatoStorage.AddPotatoes(buyTransaction); + await _token0Client.Mint(_account.Address,Web3.Convert.ToWei(buyTransaction.Weight),new CancellationToken()); + + }else{ + _logger.LogDebug("Potatoes bought successfully, mint the token representation"); + } + + }else{ + _logger.LogDebug("Reserves are balanced enough: Token0 reserves {}[token1], Token1 reserves {}[token1]", token0ReservesInToken1, token1Reserve); + } + } + + public async Task EnsureCoinSupplyMatchesRwaSupply() + { + var rwaSupply = (decimal)_potatoStorage.SupplyWeight(); + var tokenSupply = await _token0Client.TotalSupply(); + var tokenReserve = Web3.Convert.FromWei(await _token0Client.BalanceOf(_account.Address)); + + if(Math.Abs(rwaSupply-tokenSupply) < EPSIOLON){ + _logger.LogDebug("Token supply matches RWA supply"); + return; + } + + if (rwaSupply < tokenSupply) + { + var shouldBurn = tokenSupply - rwaSupply; + var burnable = Math.Min(shouldBurn,tokenReserve); // we cannot burn mor then we control + _logger.LogDebug("Should burn {} tokens, currently controlling {} tokens",shouldBurn,tokenReserve); + await _token0Client.Burn(Web3.Convert.ToWei(burnable), new CancellationToken()); + _logger.LogDebug("Burned {} tokens",burnable); + if(burnable < shouldBurn){ + _logger.LogError("Trigger rebalancing reserves and try again"); + await MaintainLiquidReservesAsync(); + await EnsureCoinSupplyMatchesRwaSupply(); + } + } + else if (rwaSupply > tokenSupply) + { + _logger.LogDebug("Must mint {} tokens", rwaSupply - tokenSupply); + await _token0Client.Mint(_account.Address, Web3.Convert.ToWei(rwaSupply - tokenSupply), new CancellationToken()); + } + + } +} -- GitLab