diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 167c0c19305203ef81cca3141ee3731daac172b6..f679630a4149e2b58e5e4b90d9a9d82aa2c4723d 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,9 @@ "extensions": [ "ms-azuretools.vscode-docker", "ms-dotnettools.csdevkit", - "hbenl.vscode-test-explorer" + "hbenl.vscode-test-explorer", + "GitHub.copilot", + "GitHub.copilot-chat" ] } }, diff --git a/.env.sample b/.env.sample index 03ce73587a1f511e8aa6fb1fae6c7fca9b1e50df..ab73312266c639475cfc9d54b9fecd47e5bcce80 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,8 @@ API_URL=https://sepolia.infura.io/v3/{yourApiKey} -ACCOUNT_PRIVATE_KEY= \ No newline at end of file +ACCOUNT_PRIVATE_KEY= +MARKETPLACE_ACCOUNT_PRIVATE_KEY= +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=roesticoin +POSTGRES_USER=roesticoin +PROMETHEUS_BASE_URL=http://prometheus:9090 diff --git a/Frontend/AssetStores/PotatoStorage.cs b/Frontend/AssetStores/PotatoStorage.cs index bf72759489a8778fbcaba25e5cca14cc78721a68..d4623cc1a96a81e2a7b3a0a03ff5538b248317dc 100644 --- a/Frontend/AssetStores/PotatoStorage.cs +++ b/Frontend/AssetStores/PotatoStorage.cs @@ -15,28 +15,14 @@ public class PotatoStorage(DbContextOptions<PotatoStorage> options) : DbContext( public async Task<PotatoTransaction> RemovePotatoes(double weight) { - var transactions = Transactions - .OrderBy(p => p.Id) - .AsQueryable(); - - var newTransaction = new PotatoTransaction(); - foreach (var transaction in transactions) + var maxRemovableWeight = SupplyWeight(); + + var removingWeight = Math.Min(maxRemovableWeight, weight); + var newTransaction = new PotatoTransaction { - var newTransactionWeight = newTransaction.Weight + transaction.Weight; - if (newTransactionWeight <= weight) - { - newTransaction.Weight += transaction.Weight; - Transactions.Remove(transaction); - } - else - { - var overshoot = newTransactionWeight - weight; - transaction.Weight -= overshoot; - newTransaction.Weight += transaction.Weight; - Transactions.Update(transaction); - } - } - + Weight = -removingWeight, + }; + await Transactions.AddAsync(newTransaction); await SaveChangesAsync(); return newTransaction; } diff --git a/Frontend/BackgroundServices/PrometheusService.cs b/Frontend/BackgroundServices/PrometheusService.cs index 677f6dd2fac3e8cf5a7d8babc25808a090b8e8f4..2dd0336c626ac79c82d0838c45f84b1592b50163 100644 --- a/Frontend/BackgroundServices/PrometheusService.cs +++ b/Frontend/BackgroundServices/PrometheusService.cs @@ -25,7 +25,8 @@ public class PrometheusService : BackgroundService Web3 web3, Account account, PotatoStorageMetrics potatoStorageMetrics, - PotatoMarketplaceMetrics potatoMarketplaceMetrics + PotatoMarketplaceMetrics potatoMarketplaceMetrics, + RebalancerMetrics rebalancerMetrics ) { _logger = logger; @@ -43,6 +44,7 @@ public class PrometheusService : BackgroundService potatoMarketplaceMetrics.CreateMetrics(); PoolV3Client poolClient = new(web3, chainSettings, account); _poolMetrics.CreateMetrics(poolClient); + rebalancerMetrics.CreateMetrics(); } protected async override Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/Frontend/BackgroundServices/RebalanceService.cs b/Frontend/BackgroundServices/RebalanceService.cs new file mode 100644 index 0000000000000000000000000000000000000000..d914a60a81586f39ef1785891efac4071f2b3f09 --- /dev/null +++ b/Frontend/BackgroundServices/RebalanceService.cs @@ -0,0 +1,46 @@ +using System.Diagnostics.Metrics; +using Frontend.Configuration; +using Frontend.Metrics; +using Nethereum.Web3; +using Nethereum.Web3.Accounts; + +namespace Frontend.BackgroundServices; + +public class RebalanceService : BackgroundService +{ + + private readonly ILogger<RebalanceService> _logger; + private readonly TimeSpan _interval = TimeSpan.FromSeconds(10); + private int startUpDelayCycles = 6; + private readonly Rebalancer _rebalancer; + + public RebalanceService( + Rebalancer rebalancer, + ILogger<RebalanceService> logger + ) + { + _logger = logger; + _rebalancer = rebalancer; + } + protected async override Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + if (startUpDelayCycles > 0) + { + _logger.LogInformation("RebalanceService is waiting to start. remaining cycles: {startUpDelayCycles}", startUpDelayCycles); + startUpDelayCycles--; + } + else + { + _logger.LogInformation("Rebalancer: EnsureCoinSupplyMatchesRwaSupplyAsync is running at {time}", DateTime.Now); + await _rebalancer.EnsureCoinSupplyMatchesRwaSupplyAsync(); + _logger.LogInformation("Rebalancer: MaintainLiquidReservesAsync is running at {time}", DateTime.Now); + await _rebalancer.MaintainLiquidReservesAsync(); + _logger.LogInformation("Rebalancer: MaintainPoolAsync is running at {time}", DateTime.Now); + await _rebalancer.MaintainPoolAsync(); + } + await Task.Delay(_interval, stoppingToken); + } + } +} \ No newline at end of file diff --git a/Frontend/Frontend.csproj b/Frontend/Frontend.csproj index d114f3697144818ed000e0681c78000801fd397b..a98f3f86eb5c5807616f8644abdaeeaf24fc50df 100644 --- a/Frontend/Frontend.csproj +++ b/Frontend/Frontend.csproj @@ -1,5 +1,6 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> + <PackageReference Include="DotNetEnv" Version="3.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" /> <PackageReference Include="Nethereum.Web3" Version="4.26.0" /> diff --git a/Frontend/Functions/LiquidityFunction.cs b/Frontend/Functions/LiquidityFunction.cs index fdc7cb27a4043fae9f6bb44c6f93eda47796ff06..0aef7fffb339576f3f6825bdd9737e76b42a0353 100644 --- a/Frontend/Functions/LiquidityFunction.cs +++ b/Frontend/Functions/LiquidityFunction.cs @@ -3,7 +3,7 @@ using Nethereum.Contracts; namespace Frontend.Functions; -[Function("liquiditry", "uint128")] +[Function("liquidity", "uint128")] public class LiquidityFunction : FunctionMessage{ } \ No newline at end of file diff --git a/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs b/Frontend/Marketplaces/Interfaces/IPotatoMarketplace.cs index 9be98564cfb6e7d7c882febbdaf0833ff10b3e92..11e4b227b0876cf6e052736cbea5e0295457132a 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 2d2024afb9a66865b58a89aaf50fb44a9b6179bb..5f0666219b90dcc9b3d7230132f1ef9337a346a1 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) { @@ -71,7 +60,7 @@ public class PotatoMarketplaceMock( public async Task<bool> Sell(PotatoTransaction transaction, string receiverAddress) { - var amount = (double)await PotatoKgWeiPrice() * transaction.Weight; + var amount = (double)await PotatoKgWeiPrice() * Math.Abs(transaction.Weight); // negative weight means selling var transferHandler = web3.Eth.GetContractTransactionHandler<TransferFunction>(); var transferFunction = new TransferFunction diff --git a/Frontend/Metrics/RebalancerMetrics.cs b/Frontend/Metrics/RebalancerMetrics.cs new file mode 100644 index 0000000000000000000000000000000000000000..aa8976dcfd7a900105404932a51bb1696b2c88d3 --- /dev/null +++ b/Frontend/Metrics/RebalancerMetrics.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.Metrics; +using Frontend.Marketplaces.Interfaces; + +namespace Frontend.Metrics; + +public class RebalancerMetrics( + Rebalancer rebalancer, + IMeterFactory meterFactory +){ + private readonly Rebalancer rebalancer = rebalancer; + private readonly IMeterFactory MeterFactory = meterFactory; + + public void CreateMetrics() + { + var meter = MeterFactory.Create("RoestiCoin"); + meter.CreateObservableGauge("Rebalancer.TradeExecutedCount", () => (int)rebalancer.RebalanceTradeExecutedCount, unit: "count"); + meter.CreateObservableGauge("Rebalancer.Reserves.ROC", () => (double)rebalancer.Token0Reserve().Result, unit: "ROC"); + meter.CreateObservableGauge("Rebalancer.Reserves.ETH", () => (double)rebalancer.Token1Reserve().Result, unit: "ETH"); + } +} \ No newline at end of file diff --git a/Frontend/Pages/Index.cshtml.cs b/Frontend/Pages/Index.cshtml.cs index 080720e6e6a8a6accec5441c98b86ad9af090380..eebbac73b727790683ad497acf33b13e5d82befa 100644 --- a/Frontend/Pages/Index.cshtml.cs +++ b/Frontend/Pages/Index.cshtml.cs @@ -1,3 +1,5 @@ +using Frontend.Assets; +using Frontend.AssetStores; using Frontend.Configuration; using Microsoft.AspNetCore.Mvc.RazorPages; using Nethereum.Web3; @@ -11,36 +13,22 @@ public class IndexModel : PageModel private readonly PoolV3Client _poolV3Client; private readonly ChainSettings _chainSettings; private readonly Account _account; + private readonly Rebalancer _rebalancer; + private readonly PotatoStorage _potatoStorage; - public IndexModel(ILogger<IndexModel> logger, Web3 web3, ChainSettings chainSettings, Account account) + public IndexModel(ILogger<IndexModel> logger, Web3 web3, ChainSettings chainSettings, Account account, Rebalancer rebalancer, PotatoStorage potatoStorage) { _logger = logger; _poolV3Client = new PoolV3Client(web3, chainSettings, account); _chainSettings = chainSettings; _account = account; + _rebalancer = rebalancer; + _potatoStorage = potatoStorage; } public void OnGet() { - string? fromTokenAddress = null; - string? toTokenAddress = null; - decimal amount = 0; - - if (Request.Query.ContainsKey("swapROCToETH")) - { - fromTokenAddress = _chainSettings.TokenAddress; - toTokenAddress = _chainSettings.WethTokenAddress; - amount = decimal.Parse(Request.Query["swapROCToETH"]); - - }else if (Request.Query.ContainsKey("swapETHToROC")) - { - fromTokenAddress = _chainSettings.WethTokenAddress; - toTokenAddress = _chainSettings.TokenAddress; - amount = decimal.Parse(Request.Query["swapETHToROC"]); - } - if (amount > 0) - { - var amountRecieved = _poolV3Client.SwapAsync(fromTokenAddress!, toTokenAddress!, amount).Result; - } + + } } diff --git a/Frontend/PoolV3Client.cs b/Frontend/PoolV3Client.cs index 1bb3b6f9ba2a418249bf5ec10368aa04e01d0c65..f925b48ac915eb7b6d548e3056d9d0be71380d0b 100644 --- a/Frontend/PoolV3Client.cs +++ b/Frontend/PoolV3Client.cs @@ -165,23 +165,24 @@ public class PoolV3Client /* this function should swap the tokens in the pool */ - public async Task<decimal> SwapAsync(string fromTokenAddress, string toTokenAddress, decimal amount, decimal amountOutMinimum = 0, uint sqrtPriceLimitX96 = 0, bool doApproval = true) + public async Task SwapAsync(string fromTokenAddress, string toTokenAddress, decimal amount, decimal amountOutMinimum = 0, uint sqrtPriceLimitX96 = 0, bool doApproval = true) { if (doApproval) { + var amountInWei=Web3.Convert.ToWei(amount); if (fromTokenAddress == Token0Address) { - if (amount > Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) + if (amountInWei > await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address)) { await Token0Client.ApproveAsync(_chainSettings.Uniswap.SwapRouterV2Address, BigInteger.Pow(2, 256) - 1); } } else { - if (amount > Web3.Convert.FromWei(await Token0Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address))) + if (amountInWei > await Token1Client.GetAllowanceAsync(_account.Address, _chainSettings.Uniswap.SwapRouterV2Address)) { - await Token0Client.ApproveAsync(_chainSettings.Uniswap.SwapRouterV2Address, BigInteger.Pow(2, 256) - 1); + await Token1Client.ApproveAsync(_chainSettings.Uniswap.SwapRouterV2Address, BigInteger.Pow(2, 256) - 1); } } } @@ -199,13 +200,6 @@ public class PoolV3Client SqrtPriceLimitX96 = sqrtPriceLimitX96, }; var exactInputSingleFunctionTxnReceipt = await contractHandler.SendRequestAndWaitForReceiptAsync(exactInputSingleFunction); - var swapEvent = exactInputSingleFunctionTxnReceipt.DecodeAllEvents<SwapEventDTO>(); - if (swapEvent.Count > 0) - { - var receivedAmount = swapEvent[0].Event.AmountOut; - return Web3.Convert.FromWei(receivedAmount); - } - throw new Exception("Swap event not found in transaction receipt"); } /// <summary> @@ -237,7 +231,7 @@ public class PoolV3Client amount = (double)(liquidity * BigInteger.Pow(2, 96) * (sqrtPriceX96 - desiredSqrtPriceX96)) / (double)(desiredSqrtPriceX96 * sqrtPriceX96); balance = await Token0Client.BalanceOf(_account.Address); } - amount *= 1.0 + (double)await QueryPoolFee() / 1000.0; // add the pool fees + amount /= 1.0 - (double)await QueryPoolFee() / 1000000.0; // add the pool fees (fee/1000)=% var bigIntAmount = new BigInteger(amount); if (balance < bigIntAmount) @@ -246,7 +240,7 @@ public class PoolV3Client } - var receipt = await SwapAsync(tokenAddresses[fromTokenIndex], tokenAddresses[(fromTokenIndex + 1) % 2], Web3.Convert.FromWei(bigIntAmount)); + await SwapAsync(tokenAddresses[fromTokenIndex], tokenAddresses[(fromTokenIndex + 1) % 2], Web3.Convert.FromWei(bigIntAmount)); return await GetPairRatio(); } diff --git a/Frontend/Program.cs b/Frontend/Program.cs index 142bd0d5574efb7c5a3930c96ab77f97631f6bdd..524f8ccd92040d221267bbfa8ecc349e14783278 100644 --- a/Frontend/Program.cs +++ b/Frontend/Program.cs @@ -9,12 +9,15 @@ using Frontend.Metrics; using Nethereum.Web3; using Nethereum.Web3.Accounts; using Microsoft.EntityFrameworkCore; +using DotNetEnv; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using Org.BouncyCastle.Utilities; using System.Numerics; +// ensure the env file is loaded +Env.Load("../.env"); // the location differ in dev and releas mode. var tokenAbiLocation = Environment.GetEnvironmentVariable("TOKEN_ABI_LOCATION")!; var tokenAbi = File.ReadAllText(tokenAbiLocation)!; @@ -26,6 +29,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); @@ -36,9 +40,11 @@ chainConfiguration.Bind(chainSettings); // Add services to the container. builder.Services.AddHostedService<PrometheusService>(); +builder.Services.AddHostedService<RebalanceService>(); builder.Services.AddSingleton<Erc20TokenMetrics>(); builder.Services.AddSingleton<PotatoStorageMetrics>(); builder.Services.AddSingleton<PotatoMarketplaceMetrics>(); +builder.Services.AddSingleton<RebalancerMetrics>(); Account account = new Account( accountPrivateKey, @@ -61,6 +67,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 => { @@ -83,6 +91,13 @@ builder.Services.AddOpenTelemetry() "RoestiCoin"); }); +builder.Services.AddSingleton<PotatoStorage>(sp => +{ + var factory = sp.GetRequiredService<IDbContextFactory<PotatoStorage>>(); + return factory.CreateDbContext(); +}); +builder.Services.AddSingleton<Rebalancer>(); + var app = builder.Build(); var potatoStorageFactory = app.Services.GetRequiredService<IDbContextFactory<PotatoStorage>>(); diff --git a/Frontend/PrometherusClient.cs b/Frontend/PrometherusClient.cs new file mode 100644 index 0000000000000000000000000000000000000000..cba3930c14614582e6fc93eec6bc1657c9bad385 --- /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 0000000000000000000000000000000000000000..2995137bbc59fc752bfd9d82937d640de358eabf --- /dev/null +++ b/Frontend/Rebalancer.cs @@ -0,0 +1,171 @@ +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.005m; + 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); + _account = 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.LogInformation("Must rebalance: Mean price {}, desired price {}", currentPrice, desiredPrice); + var quoteAfterSwap = await _pool.PerformSwapTowardsDesiredQuote(desiredPrice); + _logger.LogInformation("Quote afert swap: {}", quoteAfterSwap); + RebalanceTradeExecutedCount++; + }else{ + _logger.LogInformation("Pool is balanced enough: Mean price {}, desired price {}", currentPrice, desiredPrice); + } + } + + public async Task MaintainLiquidReservesAsync() + { + var token0Reserve = await Token0Reserve(); + var meanPoolPrice = await GetPoolPriceAsync(); + var token0ReservesInToken1 = token0Reserve * meanPoolPrice; + var token1Reserve = await Token1Reserve(); + if (token0ReservesInToken1 / token1Reserve > 2) // token0 is ROC , token1 is WETH, our reserves in ROC are twice as big as in WETH + { + _logger.LogInformation("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.LogInformation("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.LogInformation("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.LogInformation("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.LogInformation("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.LogInformation("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.LogInformation("Potatoes bought successfully, mint the token representation"); + } + + }else{ + _logger.LogInformation("Reserves are balanced enough: Token0 reserves {}[token1], Token1 reserves {}[token1]", token0ReservesInToken1, token1Reserve); + } + } + + public async Task EnsureCoinSupplyMatchesRwaSupplyAsync() + { + var rwaSupply = (decimal)_potatoStorage.SupplyWeight(); + var tokenSupply = await _token0Client.TotalSupply(); + var tokenReserve = await Token0Reserve(); + + if(Math.Abs(rwaSupply-tokenSupply) < EPSIOLON){ + _logger.LogInformation("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.LogInformation("Should burn {} tokens, currently controlling {} tokens",shouldBurn,tokenReserve); + await _token0Client.Burn(Web3.Convert.ToWei(burnable), new CancellationToken()); + _logger.LogInformation("Burned {} tokens",burnable); + if(burnable < shouldBurn){ + _logger.LogError("Trigger rebalancing reserves and try again"); + await MaintainLiquidReservesAsync(); + await EnsureCoinSupplyMatchesRwaSupplyAsync(); + } + } + else if (rwaSupply > tokenSupply) + { + _logger.LogInformation("Must mint {} tokens", rwaSupply - tokenSupply); + await _token0Client.Mint(_account.Address, Web3.Convert.ToWei(rwaSupply - tokenSupply), new CancellationToken()); + } + + } + + public async Task<decimal> Token0Reserve() + { + return Web3.Convert.FromWei(await _token0Client.BalanceOf(_account.Address)); + } + + public async Task<decimal> Token1Reserve() + { + return Web3.Convert.FromWei(await _token1Client.BalanceOf(_account.Address)); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 4a1b1aed32366b345367abd015057789e46dc13d..ee7cee240673e732d4facd0cafbfa082b54b7f68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,13 +34,21 @@ services: POSTGRES_PASSWORD: roesticoin volumes: - postgres_data:/var/lib/postgresql/data - + pgadmin: + image: dpage/pgadmin4 + restart: unless-stopped + ports: + - "5050:80" + environment: + PGADMIN_DEFAULT_EMAIL: 'roesti@example.com' + PGADMIN_DEFAULT_PASSWORD: 'TheRoestiCoinsPgAdmin' roesticoin: env_file: - .env ports: - 8080:8080 - + depends_on: + - postgres volumes: prometheus_data: postgres_data: diff --git a/grafana/dashboards/totalSupply.json b/grafana/dashboards/totalSupply.json index 9809b18d3da31e67fede240c54d44e9ac6a42723..43a88a8a475bada93ee874aaabb1ea3612f28e7d 100644 --- a/grafana/dashboards/totalSupply.json +++ b/grafana/dashboards/totalSupply.json @@ -87,7 +87,7 @@ "x": 0, "y": 0 }, - "id": 4, + "id": 6, "options": { "legend": { "calcs": [], @@ -103,12 +103,118 @@ "pluginVersion": "11.3.0+security-01", "targets": [ { - "disableTextWrap": false, "editorMode": "code", - "expr": "Ethereum_Price_Current_CHF * Uniswap_ROCWETH_price_WETH_per_ROC", + "expr": "Marketplace_Potato_Price_ETH{} * 1/Uniswap_ROCWETH_price_WETH_per_ROC", + "legendFormat": "current", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Potato_Price_ETH{}[2m]) * 1/avg_over_time(Uniswap_ROCWETH_price_WETH_per_ROC[2m])", + "hide": false, + "instant": false, + "legendFormat": "avg over 2min", + "range": true, + "refId": "B" + } + ], + "title": "ROC Price [kg Potato]", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 8, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 3, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0+security-01", + "targets": [ + { + "disableTextWrap": false, + "editorMode": "builder", + "expr": "Uniswap_ROCWETH_price_WETH_per_ROC", "fullMetaSearch": false, "includeNullMetadata": true, - "legendFormat": "price", + "legendFormat": "current price", "range": true, "refId": "A", "useBackend": false @@ -119,12 +225,152 @@ "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "avg_over_time(Ethereum_Price_Current_CHF[5m]) * avg_over_time(Uniswap_ROCWETH_price_WETH_per_ROC[5m])", + "expr": "Marketplace_Potato_Price_ETH", "hide": false, "instant": false, - "legendFormat": "Price average over 5 min", + "legendFormat": "Desired Price", "range": true, "refId": "B" + } + ], + "title": "WETH per ROC in Pool", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": -10000 + }, + { + "color": "semi-dark-green", + "value": -0.005 + }, + { + "color": "semi-dark-orange", + "value": 0.005 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 12, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "11.3.0+security-01", + "targets": [ + { + "editorMode": "code", + "expr": "Marketplace_Potato_Price_ETH{} * 1/Uniswap_ROCWETH_price_WETH_per_ROC-1", + "legendFormat": "current", + "range": true, + "refId": "A" + } + ], + "title": "Price Error", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "Shows the distribution of the reserves to rebalance the pool price", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyCHF" + }, + "overrides": [] + }, + "gridPos": { + "h": 14, + "w": 6, + "x": 12, + "y": 8 + }, + "id": 11, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "values": [ + "percent" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0+security-01", + "targets": [ + { + "editorMode": "code", + "exemplar": false, + "expr": "Rebalancer_Reserves_ROC*Uniswap_ROCWETH_price_WETH_per_ROC*Marketplace_Ethereum_Price_CHF", + "instant": true, + "legendFormat": "Reserves ROC", + "range": false, + "refId": "A" }, { "datasource": { @@ -132,16 +378,17 @@ "uid": "PBFA97CFB590B2093" }, "editorMode": "code", - "expr": "avg_over_time(Ethereum_Price_Current_CHF[2m]) * avg_over_time(Uniswap_ROCWETH_price_WETH_per_ROC[2m])", + "exemplar": false, + "expr": "Rebalancer_Reserves_ETH*Marketplace_Ethereum_Price_CHF", "hide": false, - "instant": false, - "legendFormat": "Price average over 2 min", - "range": true, - "refId": "C" + "instant": true, + "legendFormat": "Reserves (W)ETH", + "range": false, + "refId": "B" } ], - "title": "ROC Price in CHF", - "type": "timeseries" + "title": "Rebalancer Reserves", + "type": "piechart" }, { "datasource": { @@ -204,18 +451,18 @@ "overrides": [] }, "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 0 + "h": 7, + "w": 6, + "x": 18, + "y": 8 }, - "id": 3, + "id": 1, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": true + "showLegend": false }, "tooltip": { "mode": "single", @@ -225,18 +472,35 @@ "pluginVersion": "11.3.0+security-01", "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, "disableTextWrap": false, "editorMode": "builder", - "expr": "Uniswap_ROCWETH_price_WETH_per_ROC", + "expr": "RoestiCoin_Supply_Total_ROC", "fullMetaSearch": false, "includeNullMetadata": true, - "legendFormat": "__auto", + "legendFormat": "Total Supply of ROC", "range": true, "refId": "A", "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "Storage_Potato_Supply_Weight_kg", + "hide": false, + "instant": false, + "legendFormat": "Potatos in Stock [kg]", + "range": true, + "refId": "B" } ], - "title": "WETH ROC", + "title": "Total Supply", "type": "timeseries" }, { @@ -283,6 +547,103 @@ } }, "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 15 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "last" + ], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0+security-01", + "targets": [ + { + "editorMode": "code", + "expr": "Storage_Potato_Supply_Weight_kg/RoestiCoin_Supply_Total_ROC", + "legendFormat": "backing", + "range": true, + "refId": "A" + } + ], + "title": "Potato [kg]/ROC", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "description": "averaged over 2 minutes", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 8, + "mappings": [], "thresholds": { "mode": "absolute", "steps": [ @@ -300,18 +661,110 @@ "overrides": [] }, "gridPos": { - "h": 8, + "h": 14, "w": 12, "x": 0, - "y": 8 + "y": 16 }, - "id": 2, + "id": 9, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "11.3.0+security-01", + "targets": [ + { + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Potato_Price_ETH[2m])", + "legendFormat": "current", + "range": true, + "refId": "A" + } + ], + "title": "Price of 1kg Potato [ETH]", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 22 + }, + "id": 4, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true }, "tooltip": { "mode": "single", @@ -322,18 +775,43 @@ "targets": [ { "disableTextWrap": false, - "editorMode": "builder", - "expr": "Ethereum_Price_Current_CHF", + "editorMode": "code", + "expr": "Marketplace_Ethereum_Price_CHF * Uniswap_ROCWETH_price_WETH_per_ROC", "fullMetaSearch": false, "includeNullMetadata": true, - "legendFormat": "__auto", + "legendFormat": "price", "range": true, "refId": "A", "useBackend": false + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Ethereum_Price_CHF[5m]) * avg_over_time(Uniswap_ROCWETH_price_WETH_per_ROC[5m])", + "hide": false, + "instant": false, + "legendFormat": "Price average over 5 min", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Ethereum_Price_CHF[2m]) * avg_over_time(Uniswap_ROCWETH_price_WETH_per_ROC[2m])", + "hide": false, + "instant": false, + "legendFormat": "Price average over 2 min", + "range": true, + "refId": "C" } ], - "title": "CHF/ETH course", - "transparent": true, + "title": "ROC Price in CHF", "type": "timeseries" }, { @@ -398,17 +876,17 @@ }, "gridPos": { "h": 8, - "w": 12, - "x": 12, - "y": 8 + "w": 6, + "x": 18, + "y": 22 }, - "id": 1, + "id": 8, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", - "showLegend": false + "showLegend": true }, "tooltip": { "mode": "single", @@ -417,24 +895,41 @@ }, "pluginVersion": "11.3.0+security-01", "targets": [ + { + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Ethereum_Price_CHF[5m])", + "legendFormat": "ETH in CHF averaged 5min", + "range": true, + "refId": "A" + }, { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, - "disableTextWrap": false, - "editorMode": "builder", - "expr": "RoestiCoin_Supply_Total_ROC", - "fullMetaSearch": false, - "includeNullMetadata": true, - "legendFormat": "__auto", + "editorMode": "code", + "expr": "avg_over_time(Marketplace_Ethereum_Price_CHF[2m])", + "hide": false, + "instant": false, + "legendFormat": "ETH in CHF averaged 2min", "range": true, - "refId": "A", - "useBackend": false + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "Marketplace_Ethereum_Price_CHF", + "hide": false, + "instant": false, + "legendFormat": "ETH in CHF", + "range": true, + "refId": "C" } ], - "title": "Total Supply (ROC)", - "transparent": true, + "title": "ETH price in CHF", "type": "timeseries" } ], @@ -450,8 +945,8 @@ }, "timepicker": {}, "timezone": "browser", - "title": "Total Supply of ROC", + "title": "ROC - RöstiCoin Overview Dashboard", "uid": "ce3nybz6khudcd", - "version": 2, + "version": 15, "weekStart": "" } \ No newline at end of file