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