From c066a98bcacbb67428e91ce634f75c57e37bde52 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <0xtekgrinder@protonmail.com> Date: Sun, 6 Oct 2024 13:56:06 +0200 Subject: [PATCH] feat: initial version of the gnoswap contracts with deposit, withdraw and permissioned creation / rebalancing of the positions. --- contracts/getters.gno | 32 +++++++ contracts/gno.mod | 7 ++ contracts/token_register.gno | 149 ++++++++++++++++++++++++++++++ contracts/type.gno | 13 +++ contracts/utils.gno | 59 ++++++++++++ contracts/vault.gno | 170 +++++++++++++++++++++++++++++++++++ contracts/vault_test.gno | 1 + 7 files changed, 431 insertions(+) create mode 100644 contracts/getters.gno create mode 100644 contracts/gno.mod create mode 100644 contracts/token_register.gno create mode 100644 contracts/type.gno create mode 100644 contracts/utils.gno create mode 100644 contracts/vault.gno create mode 100644 contracts/vault_test.gno diff --git a/contracts/getters.gno b/contracts/getters.gno new file mode 100644 index 0000000..4e1fbfe --- /dev/null +++ b/contracts/getters.gno @@ -0,0 +1,32 @@ +package gnoswap_optimizer + +func GetVault(tokenId uint64) Vault { + vault := mustGetVault(tokenId) + return *vault +} + +func GetVaultPaused(tokenId uint64) bool { + vault := mustGetVault(tokenId) + return vault.paused +} + +func GetVaultToken0(tokenId uint64) string { + vault := mustGetVault(tokenId) + return vault.token0 +} + +func GetVaultToken1(tokenId uint64) string { + vault := mustGetVault(tokenId) + return vault.token1 +} + +func GetVaultFee(tokenId uint64) uint32 { + vault := mustGetVault(tokenId) + return vault.fee +} + +func BalanceOf(tokenId uint64, account string) string { + vault := mustGetVault(tokenId) + balance := mustGetBalance(vault, account) + return balance.ToString() +} diff --git a/contracts/gno.mod b/contracts/gno.mod new file mode 100644 index 0000000..c9b1731 --- /dev/null +++ b/contracts/gno.mod @@ -0,0 +1,7 @@ +module gno.land/r/gnoswap_optimizer + +require ( + gno.land/p/demo/avl v0.0.0-latest + gno.land/p/demo/ownable v0.0.0-latest + gno.land/r/gnoswap/v2/position v0.0.0-latest +) diff --git a/contracts/token_register.gno b/contracts/token_register.gno new file mode 100644 index 0000000..a5564b4 --- /dev/null +++ b/contracts/token_register.gno @@ -0,0 +1,149 @@ +package gnoswap_optimizer + +import ( + "fmt" + "std" + + pusers "gno.land/p/demo/users" + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v2/consts" +) + +type GRC20Interface interface { + Transfer() func(to pusers.AddressOrName, amount uint64) + TransferFrom() func(from, to pusers.AddressOrName, amount uint64) + BalanceOf() func(owner pusers.AddressOrName) uint64 + Approve() func(spender pusers.AddressOrName, amount uint64) +} + +var ( + registered = make(map[string]GRC20Interface) + locked = false // mutex +) + +func GetRegisteredTokens() []string { + tokens := make([]string, 0, len(registered)) + for k := range registered { + tokens = append(tokens, k) + } + return tokens +} + +func RegisterGRC20Interface(pkgPath string, igrc20 GRC20Interface) { + caller := std.GetOrigCaller() + if caller != consts.TOKEN_REGISTER { + panic(fmt.Sprintf("unauthorized address(%s) to register", caller.String())) + } + + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if found { + panic(fmt.Sprintf("pkgPath(%s) already registered", pkgPath)) + } + + registered[pkgPath] = igrc20 +} + +func UnregisterGRC20Interface(pkgPath string) { + // only admin can unregister + caller := std.GetOrigCaller() + if caller != consts.TOKEN_REGISTER { + panic(fmt.Sprintf("unauthorized address(%s) to unregister", caller.String())) + } + + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if found { + delete(registered, pkgPath) + } +} + +func transferByRegisterCall(pkgPath string, to std.Address, amount string) bool { + amountParsed := checkAmountRange(amount) + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(fmt.Sprintf("pkgPath(%s) not found", pkgPath)) + } + + if !locked { + locked = true + registered[pkgPath].Transfer()(pusers.AddressOrName(to), amountParsed) + + defer func() { + locked = false + }() + } else { + panic("expected locked to be false") + } + return true +} + +func transferFromByRegisterCall(pkgPath string, from, to std.Address, amount string) bool { + amountParsed := checkAmountRange(amount) + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(fmt.Sprintf("pkgPath(%s) not found", pkgPath)) + } + + if !locked { + locked = true + registered[pkgPath].TransferFrom()(pusers.AddressOrName(from), pusers.AddressOrName(to), amountParsed) + + defer func() { + locked = false + }() + } else { + panic("expected locked to be false") + } + return true +} + +func balanceOfByRegisterCall(pkgPath string, owner std.Address) uint64 { + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(fmt.Sprintf("pkgPath(%s) not found", pkgPath)) + } + + balance := registered[pkgPath].BalanceOf()(pusers.AddressOrName(owner)) + return balance +} + +func approveByRegisterCall(pkgPath string, spender std.Address, amount string) bool { + amountParsed := checkAmountRange(amount) + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(fmt.Sprintf("pkgPath(%s) not found", pkgPath)) + } + + registered[pkgPath].Approve()(pusers.AddressOrName(spender), amountParsed) + + return true +} + +func handleNative(pkgPath string) string { + if pkgPath == consts.GNOT { + return consts.WRAPPED_WUGNOT + } + + return pkgPath +} + +func checkAmountRange(amount string) uint64 { + // check amount is in uint64 range + amountParsed, err := u256.FromDecimal(amount) + if err != nil { + panic(fmt.Sprintf("amount(%s) is not in uint64 range", amount)) + } + + return amountParsed.Uint64() +} diff --git a/contracts/type.gno b/contracts/type.gno new file mode 100644 index 0000000..b54ed13 --- /dev/null +++ b/contracts/type.gno @@ -0,0 +1,13 @@ +package gnoswap_optimizer + +import ( + "gno.land/p/demo/avl" +) + +type Vault struct { + balances *avl.Tree // address -> *u256.Uint + paused bool + token0 string + token1 string + fee uint32 +} diff --git a/contracts/utils.gno b/contracts/utils.gno new file mode 100644 index 0000000..8489f19 --- /dev/null +++ b/contracts/utils.gno @@ -0,0 +1,59 @@ +package gnoswap_optimizer + +import ( + "fmt" + "strconv" + + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v2/position" +) + +func assertVaultPaused(vault *Vault) { + if vault.paused { + panic("Vault is paused") + } +} + +func mustGetVault(tokenId uint64) *Vault { + vault, exists := vaults.Get(strconv.FormatUint(tokenId, 10)) + if !exists { + panic(fmt.Sprintf("Vault %d not found", tokenId)) + } + return vault.(*Vault) +} + +func mustGetBalance(vault *Vault, account string) *u256.Uint { + balance, exists := vault.balances.Get(account) + if !exists { + panic(fmt.Sprintf("Balance for account %s not found", account)) + } + parsedAmount, err := u256.FromDecimal(balance.(string)) + if err != nil { + panic(err) + } + return parsedAmount +} + +func increaseBalance(vault *Vault, account string, amount string) { + parsedAmount, err := u256.FromDecimal(amount) + if err != nil { + panic(err) + } + + currentBalance := mustGetBalance(vault, account) + currentBalance.Add(currentBalance, parsedAmount) + vault.balances.Set(account, currentBalance.ToString()) +} + +// get a percentage of the total liquidity as an uint64 +func computeLiquidityRatio(tokenId uint64, balance *u256.Uint) uint64 { + totalLiquidity := position.PositionGetPositionLiquidity(tokenId) + + // get a percentage of the total liquidity as an uint64 + percentage := new(u256.Uint) + percentage.Set(balance) + percentage.Mul(percentage, u256.NewUint(100)) + percentage.Div(percentage, totalLiquidity) + + return percentage.Uint64() +} diff --git a/contracts/vault.gno b/contracts/vault.gno new file mode 100644 index 0000000..f5ad6cc --- /dev/null +++ b/contracts/vault.gno @@ -0,0 +1,170 @@ +package gnoswap_optimizer + +import ( + "std" + "strconv" + "time" + + "gno.land/p/demo/acl" + "gno.land/p/demo/avl" + u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v2/position" +) + +var ( + vaults avl.Tree // tokenId -> *Vault + accessControl acl.Directory +) + +func init() { + accessControl.AddUserPerm(std.PrevRealm().Addr(), "role", "admin") +} + +/** + * USER FUNCTIONS + */ + +// DepositMulti deposits tokens into a vault +// @param tokenId uint64 the vault id +// @param _amount0 string the amount of token0 to deposit +// @param _amount1 string the amount of token1 to deposit +func DepositMulti(tokenId uint64, _amount0 string, _amount1 string) { + vault := mustGetVault(tokenId) + assertVaultPaused(vault) + + caller := std.PrevRealm().Addr() + + // Pull tokens from the caller + transferFromByRegisterCall(vault.token0, caller, std.CurrentRealm().Addr(), _amount0) + transferFromByRegisterCall(vault.token1, caller, std.CurrentRealm().Addr(), _amount1) + + // Approve the pool contract to spend the tokens + poolAddr := std.DerivePkgAddr("gno.land/r/gnoswap/v2/pool") + approveByRegisterCall(vault.token0, poolAddr, _amount0) + approveByRegisterCall(vault.token1, poolAddr, _amount1) + + _, liquidity, _, _, _ := position.IncreaseLiquidity(tokenId, _amount0, _amount1, _amount0, _amount1, time.Now().Unix()+1) + + increaseBalance(vault, caller.String(), liquidity) + + std.Emit("Deposit", caller.String(), strconv.FormatUint(tokenId, 10), _amount0, _amount1, liquidity) +} + +// DepositSingle deposits a single token into a vault +// @param tokenId uint64 the vault id +// @param token string the token to deposit +// @param amount string the amount of token to deposit +func DepositSingle(tokenId uint64, token string, amount string) { + vault := mustGetVault(tokenId) + assertVaultPaused(vault) + + // TODO swap token to token0 or token1 if needed +} + +// Withdraw withdraws tokens from a vault +// @param tokenId uint64 the vault id +// @param amount string the amount of liquidity to withdraw +func Withdraw(tokenId uint64, amount string) { + vault := mustGetVault(tokenId) + caller := std.PrevRealm().Addr() + balance := mustGetBalance(vault, caller.String()) + parsedAmount, err := u256.FromDecimal(amount) + if err != nil { + panic(err) + } + + liquidityRatio := computeLiquidityRatio(tokenId, balance) + _, liquidity, _, _, amount0, amount1, _ := position.DecreaseLiquidity(tokenId, liquidityRatio, u256.NewUint(0).ToString(), u256.NewUint(0).ToString(), time.Now().Unix()+1, false) + + // Transfer tokens to the caller + transferByRegisterCall(vault.token0, caller, amount0) + transferByRegisterCall(vault.token1, caller, amount1) + + std.Emit("Withdraw", caller.String(), strconv.FormatUint(tokenId, 10), amount0, amount1, liquidity) +} + +func Claim(tokenId uint64) { + vault := mustGetVault(tokenId) +} + +/** + * ADMIN FUNCTIONS + */ + +// NewVault creates a new vault +// @param token0 string the address of the first token +// @param token1 string the address of the second token +// @param fee uint32 the fee of the pool +// @param tickLower int32 the lower tick of the pool +// @param tickUpper int32 the upper tick of the pool +// @param _amount0 string the amount of token0 to deposit +// @param _amount1 string the amount of token1 to deposit +func NewVault( + token0 string, + token1 string, + fee uint32, + tickLower int32, + tickUpper int32, + _amount0 string, // *u256.Uint + _amount1 string, // *u256.Uint +) { + caller := std.PrevRealm().Addr() + accessControl.HasRole(caller, "admin") + + // Pull tokens from the caller + transferFromByRegisterCall(token0, std.PrevRealm().Addr(), std.CurrentRealm().Addr(), _amount0) + transferFromByRegisterCall(token1, std.PrevRealm().Addr(), std.CurrentRealm().Addr(), _amount1) + + // Approve the pool contract to spend the tokens + poolAddr := std.DerivePkgAddr("gno.land/r/gnoswap/v2/pool") + approveByRegisterCall(token0, poolAddr, _amount0) + approveByRegisterCall(token1, poolAddr, _amount1) + + tokenId, liquidity, _, _ := position.Mint(token0, token1, fee, tickLower, tickUpper, _amount0, _amount1, _amount0, _amount1, time.Now().Unix()+1, std.CurrentRealm().Addr().String()) + + // Create the vault and set the admin's balance + vault := &Vault{ + balances: avl.NewTree(), + paused: false, + token0: token0, + token1: token1, + fee: fee, + } + vault.balances.Set(caller.String(), liquidity) + + vaults.Set(strconv.FormatUint(tokenId, 10), vault) + + std.Emit("NewVault", token0, token1, strconv.FormatUint(uint64(fee), 10), strconv.FormatUint(tokenId, 10)) +} + +// TogglePause pause deposits for a vault +// @param tokenId uint64 the vault id +func TogglePause(tokenId uint64) { + accessControl.HasRole(std.PrevRealm().Addr(), "admin") + + vault := mustGetVault(tokenId) + vault.paused = !vault.paused +} + +// AddKeeper adds a keeper that can rebalance vaults +// @param keeper Address the address of the keeper +func AddKeeper(keeper std.Address) { + accessControl.HasRole(std.PrevRealm().Addr(), "admin") + + accessControl.AddUserPerm(keeper, "role", "keeper") +} + +/** + * KEEPER FUNCTIONS + */ + +// Rebalance rebalances the position of a vault +// @param tokenId uint64 the vault id +func Rebalance(tokenId uint64) { + accessControl.HasRole(std.PrevRealm().Addr(), "keeper") + + vault := mustGetVault(tokenId) + assertVaultPaused(vault) + + // TODO rebalance the vault +} diff --git a/contracts/vault_test.gno b/contracts/vault_test.gno new file mode 100644 index 0000000..0f2f3d0 --- /dev/null +++ b/contracts/vault_test.gno @@ -0,0 +1 @@ +package gnoswap_optimizer