From 1429e2b58cb32235703ddeec77519f8e5f031a11 Mon Sep 17 00:00:00 2001 From: Blake <104744707+r3v4s@users.noreply.github.com> Date: Sun, 19 Jan 2025 16:41:17 +0900 Subject: [PATCH] feat/manage protocol fee list (#464) * feat: manage accumulated protocol_fee list * feat: `pool` adds token(+amount) to protocol_fee * feat: `router` adds token(+amount) to protocol_fee * feat: `staker` adds token(+amount) to protocol_fee * fix: amount and balance checking --------- Co-authored-by: Dongwon <74406335+dongwon8247@users.noreply.github.com> Co-authored-by: Lee ByeongJun --- _deploy/r/gnoswap/common/access.gno | 8 + pool/pool_manager.gno | 4 + pool/protocol_fee_withdrawal.gno | 4 + pool/protocol_fee_withdrawal_test.gno | 2 +- protocol_fee/errors.gno | 9 +- protocol_fee/protocol_fee.gno | 5 - protocol_fee/protocol_fee_test.gno | 2 +- protocol_fee/token_list_with_amount.gno | 137 +++++++++++ protocol_fee/token_list_with_amount_test.gno | 235 +++++++++++++++++++ protocol_fee/utils.gno | 11 +- router/protocol_fee_swap.gno | 3 + staker/protocol_fee_unstaking.gno | 3 + 12 files changed, 406 insertions(+), 17 deletions(-) create mode 100644 protocol_fee/token_list_with_amount.gno create mode 100644 protocol_fee/token_list_with_amount_test.gno diff --git a/_deploy/r/gnoswap/common/access.gno b/_deploy/r/gnoswap/common/access.gno index 0297c4a60..30a0c8e36 100644 --- a/_deploy/r/gnoswap/common/access.gno +++ b/_deploy/r/gnoswap/common/access.gno @@ -58,6 +58,14 @@ func RouterOnly(caller std.Address) error { return nil } +// PoolOnly checks if the caller is the pool contract. +func PoolOnly(caller std.Address) error { + if caller != consts.POOL_ADDR { + return ufmt.Errorf(ErrNoPermission, caller.String()) + } + return nil +} + // PositionOnly checks if the caller is the position contract. func PositionOnly(caller std.Address) error { if caller != consts.POSITION_ADDR { diff --git a/pool/pool_manager.gno b/pool/pool_manager.gno index 8edcb1af9..2248ea447 100644 --- a/pool/pool_manager.gno +++ b/pool/pool_manager.gno @@ -11,7 +11,10 @@ import ( "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" + en "gno.land/r/gnoswap/v1/emission" + pf "gno.land/r/gnoswap/v1/protocol_fee" + "gno.land/r/gnoswap/v1/gns" ) @@ -189,6 +192,7 @@ func CreatePool( if poolCreationFee > 0 { gns.TransferFrom(a2u(std.PrevRealm().Addr()), a2u(consts.PROTOCOL_FEE_ADDR), poolCreationFee) + pf.AddToProtocolFee(consts.GNS_PATH, poolCreationFee) std.Emit( "PoolCreationFee", diff --git a/pool/protocol_fee_withdrawal.gno b/pool/protocol_fee_withdrawal.gno index d7d208b9b..80dc72451 100644 --- a/pool/protocol_fee_withdrawal.gno +++ b/pool/protocol_fee_withdrawal.gno @@ -5,8 +5,10 @@ import ( "gno.land/p/demo/ufmt" u256 "gno.land/p/gnoswap/uint256" + "gno.land/r/gnoswap/v1/common" "gno.land/r/gnoswap/v1/consts" + pf "gno.land/r/gnoswap/v1/protocol_fee" ) // withdrawalFeeBPS is the fee that is charged when a user withdraws their collected fees @@ -68,9 +70,11 @@ func HandleWithdrawalFee( token0Teller := common.GetTokenTeller(token0Path) checkTransferError(token0Teller.TransferFrom(positionCaller, consts.PROTOCOL_FEE_ADDR, feeAmount0.Uint64())) + pf.AddToProtocolFee(token0Path, feeAmount0.Uint64()) token1Teller := common.GetTokenTeller(token1Path) checkTransferError(token1Teller.TransferFrom(positionCaller, consts.PROTOCOL_FEE_ADDR, feeAmount1.Uint64())) + pf.AddToProtocolFee(token1Path, feeAmount1.Uint64()) prevAddr, prevPkgPath := getPrevAsString() std.Emit( diff --git a/pool/protocol_fee_withdrawal_test.gno b/pool/protocol_fee_withdrawal_test.gno index 7cbac5d39..53f9e0668 100644 --- a/pool/protocol_fee_withdrawal_test.gno +++ b/pool/protocol_fee_withdrawal_test.gno @@ -46,7 +46,7 @@ func TestHandleWithdrawalFee(t *testing.T) { HandleWithdrawalFee(0, "pkgPath", "1000", "pkgPath", "1000", "poolPath", users.Resolve(admin)) }, verify: nil, - expected: "[GNOSWAP-COMMON-004] token is not registered || token(pkgPath) is not registered", + expected: "[GNOSWAP-COMMON-004] token is not registered || token(pkgPath)", shouldPanic: true, }, { diff --git a/protocol_fee/errors.gno b/protocol_fee/errors.gno index 6ed788f5d..bc8e9fa7f 100644 --- a/protocol_fee/errors.gno +++ b/protocol_fee/errors.gno @@ -7,12 +7,9 @@ import ( ) var ( - errNoPermission = errors.New("[GNOSWAP-PROTOCOL_FEE-001] caller has no permission") - errNotRegistered = errors.New("[GNOSWAP-PROTOCOL_FEE-002] not registered token") - errAlreadyRegistered = errors.New("[GNOSWAP-PROTOCOL_FEE-003] already registered token") - errLocked = errors.New("[GNOSWAP-PROTOCOL_FEE-004] can't transfer token while locked") - errInvalidInput = errors.New("[GNOSWAP-PROTOCOL_FEE-005] invalid input data") - errInvalidPct = errors.New("[GNOSWAP-PROTOCOL_FEE-006] invalid percentage") + errNoPermission = errors.New("[GNOSWAP-PROTOCOL_FEE-001] caller has no permission") + errInvalidPct = errors.New("[GNOSWAP-PROTOCOL_FEE-002] invalid percentage") + errInvalidAmount = errors.New("[GNOSWAP-PROTOCOL_FEE-003] invalid amount") ) func addDetailToError(err error, detail string) string { diff --git a/protocol_fee/protocol_fee.gno b/protocol_fee/protocol_fee.gno index 7ee21f4b5..4fbd76eac 100644 --- a/protocol_fee/protocol_fee.gno +++ b/protocol_fee/protocol_fee.gno @@ -135,11 +135,6 @@ func ClearAccuTransferToGovStaker() { accuToGovStaker = avl.NewTree() } -// assertOnlyNotHalted panics if the contract is halted. -func assertOnlyNotHalted() { - common.IsHalted() -} - // addAccuToGovStaker adds the amount to the accuToGovStaker by token path. func addAccuToGovStaker(path string, amount uint64) { before := GetAccuTransferToGovStakerByTokenPath(path) diff --git a/protocol_fee/protocol_fee_test.gno b/protocol_fee/protocol_fee_test.gno index 4feb35538..9cb75b770 100644 --- a/protocol_fee/protocol_fee_test.gno +++ b/protocol_fee/protocol_fee_test.gno @@ -61,7 +61,7 @@ func TestSetDevOpsPctByAdminInvalidFee(t *testing.T) { uassert.PanicsWithMessage( t, - `[GNOSWAP-PROTOCOL_FEE-006] invalid percentage || pct(100001) should not be bigger than 10000`, + `[GNOSWAP-PROTOCOL_FEE-002] invalid percentage || pct(100001) should not be bigger than 10000`, func() { SetDevOpsPctByAdmin(100001) }, diff --git a/protocol_fee/token_list_with_amount.gno b/protocol_fee/token_list_with_amount.gno new file mode 100644 index 000000000..ef600d73f --- /dev/null +++ b/protocol_fee/token_list_with_amount.gno @@ -0,0 +1,137 @@ +package protocol_fee + +import ( + "std" + "strings" + + "gno.land/p/demo/ufmt" + + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/consts" +) + +var ( + tokenListWithAmount = make(map[string]uint64) // tokenPath -> amount +) + +// TokenList returns only the list of token path. +// If positive is true, it returns only the token path with amount > 0. +// If positive is false, it returns all the token path. +func TokenList(positive bool) []string { + tokens := []string{} + + for tokenPath, amount := range tokenListWithAmount { + if positive && amount == 0 { + continue + } + + tokens = append(tokens, tokenPath) + } + + return tokens +} + +// TokenListWithAmount returns the token path and amount. +func TokenListWithAmount() map[string]uint64 { + return tokenListWithAmount +} + +// AddToProtocolFee adds the amount to the tokenListWithAmount +// Only `pool + router + staker` can execute this function. +func AddToProtocolFee(tokenPath string, amount uint64) { + assertOnlyPoolRouterStaker() + tokenListWithAmount[tokenPath] += amount +} + +// ClearTokenListWithAmount clears the tokenListWithAmount. +// only `gov/staker` can execute this function. +func ClearTokenListWithAmount() { + assertOnlyGovStaker() + clearTokenListWithAmount() +} + +// TransferProtocolFee transfers the protocol fee to devOps and gov/staker. +// only `gov/staker` can execute this function. +// It returns list of token with amount has been sent to gov/staker. +func TransferProtocolFee() map[string]uint64 { + assertOnlyGovStaker() + + sentToDevOps := []string{} + sentToGovStaker := []string{} + toReturn := map[string]uint64{} + + for token, amount := range tokenListWithAmount { + balance := common.BalanceOf(token, consts.PROTOCOL_FEE_ADDR) + + // anyone can just send certain grc20 token to `protocol_fee` contract + // therefore, we don't need any guard logic to check whether protocol_fee's xxx token balance is equal to `amount` + // however, amount always should be less than or equal to balance + if amount > balance { + panic(addDetailToError( + errInvalidAmount, + ufmt.Sprintf("amount: %d should be less than or equal to balance: %d", amount, balance), + )) + } + + if amount > 0 { + toDevOps := balance * devOpsPct / 10000 // default 0% + toGovStaker := balance - toDevOps // default 100% + + tokenTeller := common.GetTokenTeller(token) + if toDevOps > 0 { + tokenTeller.Transfer(consts.DEV_OPS, toDevOps) + sentToDevOps = append(sentToDevOps, makeEventString(token, toDevOps)) + } + + if toGovStaker > 0 { + tokenTeller.Transfer(consts.GOV_STAKER_ADDR, toGovStaker) + sentToGovStaker = append(sentToGovStaker, makeEventString(token, toGovStaker)) + + toReturn[token] = toGovStaker + } + } + } + + clearTokenListWithAmount() + + prevAddr, prevRealm := getPrev() + std.Emit( + "TransferProtocolFee", + "prevAddr", prevAddr, + "prevRealm", prevRealm, + "toDevOps", strings.Join(sentToDevOps, ","), + "toGovStaker", strings.Join(sentToGovStaker, ","), + ) + + return toReturn +} + +// clearTokenListWithAmount clears the tokenListWithAmount. +func clearTokenListWithAmount() { + tokenListWithAmount = map[string]uint64{} +} + +// assertOnlyPoolRouterStaker panics if the caller is not the pool, router, or staker contract. +func assertOnlyPoolRouterStaker() { + caller := std.PrevRealm().Addr() + + poolOnlyErr := common.PoolOnly(caller) + routerOnlyErr := common.RouterOnly(caller) + stakerOnlyErr := common.StakerOnly(caller) + + if poolOnlyErr != nil && routerOnlyErr != nil && stakerOnlyErr != nil { + panic(errNoPermission) + } +} + +// assertOnlyGovStaker panics if the caller is not the gov/staker contract. +func assertOnlyGovStaker() { + caller := std.PrevRealm().Addr() + if err := common.GovStakerOnly(caller); err != nil { + panic(err.Error()) + } +} + +func makeEventString(tokenPath string, amount uint64) string { + return tokenPath + "*FEE*" + ufmt.Sprintf("%d", amount) +} diff --git a/protocol_fee/token_list_with_amount_test.gno b/protocol_fee/token_list_with_amount_test.gno new file mode 100644 index 000000000..12d1dd0f9 --- /dev/null +++ b/protocol_fee/token_list_with_amount_test.gno @@ -0,0 +1,235 @@ +package protocol_fee + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" + + "gno.land/r/gnoswap/v1/common" + "gno.land/r/gnoswap/v1/consts" + +) + +var ( + dummyRealm = std.NewCodeRealm("gno.land/r/dummy") + + adminAddr = consts.ADMIN + adminUser = common.AddrToUser(adminAddr) + adminRealm = std.NewUserRealm(adminAddr) +) + +func TestTokenList(t *testing.T) { + tokenListWithAmount = map[string]uint64{ + "gno.land/r/foo": 100, + "gno.land/r/bar": 0, + "gno.land/r/baz": 200, + } + + uassert.Equal(t, len(TokenList(true)), 2) + uassert.Equal(t, len(TokenList(false)), 3) +} + +func TestAddToProtocolFee(t *testing.T) { + tokenListWithAmount = map[string]uint64{} + + tests := []struct { + name string + tokenPath string + amount uint64 + want uint64 + }{ + { + name: "add foo to protocol fee", + tokenPath: "gno.land/r/foo", + amount: 100, + want: 100, + }, + { + name: "add baz to protocol fee", + tokenPath: "gno.land/r/baz", + amount: 50, + want: 50, + }, + { + name: "add more baz to protocol fee", + tokenPath: "gno.land/r/baz", + amount: 10, + want: 60, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + std.TestSetRealm(std.NewCodeRealm(consts.POOL_PATH)) + AddToProtocolFee(test.tokenPath, test.amount) + + uassert.Equal(t, tokenListWithAmount[test.tokenPath], test.want) + }) + } +} + +func TestClearTokenListWithAmount(t *testing.T) { + tokenListWithAmount = map[string]uint64{ + "gno.land/r/foo": 100, + "gno.land/r/baz": 200, + } + + tests := []struct { + name string + prevRealm std.Realm + want map[string]uint64 + shouldPanic bool + panicMsg string + }{ + { + name: "no permission to clear", + prevRealm: dummyRealm, + shouldPanic: true, + panicMsg: "caller(g1lvx5ssxvuz5tttx6uza3myv8xy6w36a46fv7sy) has no permission", + }, + { + name: "clear protocol fee", + prevRealm: std.NewCodeRealm(consts.GOV_STAKER_PATH), + want: map[string]uint64{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + std.TestSetRealm(test.prevRealm) + + if test.shouldPanic { + uassert.PanicsWithMessage(t, test.panicMsg, func() { + ClearTokenListWithAmount() + }) + } else { + ClearTokenListWithAmount() + uassert.Equal(t, len(tokenListWithAmount), len(test.want)) + } + }) + } +} + +func TestTransferProtocolFee(t *testing.T) { + // change devOpsPct to 49% (51% is for gov/staker) + devOpsPct = 4900 + + // send(and add) to protocol fee + transferToProtocolFee(t, "gno.land/r/onbloc/foo", 100) + transferToProtocolFee(t, "gno.land/r/onbloc/baz", 201) + + // call TransferProtocolFee + std.TestSetRealm(std.NewCodeRealm(consts.GOV_STAKER_PATH)) + + devOpsOldFoo := common.BalanceOf("gno.land/r/onbloc/foo", consts.DEV_OPS) + devOpsOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", consts.DEV_OPS) + govStkaerOldFoo := common.BalanceOf("gno.land/r/onbloc/foo", consts.GOV_STAKER_ADDR) + govStkaerOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", consts.GOV_STAKER_ADDR) + uassert.Equal(t, devOpsOldFoo, uint64(0)) + uassert.Equal(t, devOpsOldBaz, uint64(0)) + uassert.Equal(t, govStkaerOldFoo, uint64(0)) + uassert.Equal(t, govStkaerOldBaz, uint64(0)) + + sentToGovStaker := TransferProtocolFee() + // foo 100 + // -> devOps 49% => 49 + // -> gov/staker 51% => 51 + + // baz 201 + // -> devOps 49% => 98 + // -> gov/staker 51% => 103 + + // emitted event + // EVENTS: [{"type":"TransferProtocolFee","attrs":[{"key":"prevAddr","value":"g17e3ykyqk9jmqe2y9wxe9zhep3p7cw56davjqwa"},{"key":"prevRealm","value":"gno.land/r/gnoswap/v1/gov/staker"},{"key":"toDevOps","value":"gno.land/r/onbloc/foo*FEE*49,gno.land/r/onbloc/baz*FEE*98"},{"key":"toGovStaker","value":"gno.land/r/onbloc/foo*FEE*51,gno.land/r/onbloc/baz*FEE*103"}],"pkg_path":"gno.land/r/gnoswap/v1/protocol_fee","func":"TransferProtocolFee"}] + + devOpsNewFoo := common.BalanceOf("gno.land/r/onbloc/foo", consts.DEV_OPS) + devOpsNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", consts.DEV_OPS) + uassert.Equal(t, devOpsNewFoo, uint64(49)) + uassert.Equal(t, devOpsNewBaz, uint64(98)) + + govStkaerNewFoo := common.BalanceOf("gno.land/r/onbloc/foo", consts.GOV_STAKER_ADDR) + govStkaerNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", consts.GOV_STAKER_ADDR) + uassert.Equal(t, govStkaerNewFoo, uint64(51)) + uassert.Equal(t, govStkaerNewBaz, uint64(103)) + + uassert.Equal(t, len(sentToGovStaker), 2) + uassert.Equal(t, sentToGovStaker["gno.land/r/onbloc/foo"], uint64(51)) + uassert.Equal(t, sentToGovStaker["gno.land/r/onbloc/baz"], uint64(103)) +} + +func TestAssertOnlyPoolRouterStaker(t *testing.T) { + tests := []struct { + name string + prevRealm std.Realm + shouldPanic bool + panicMsg string + }{ + { + name: "caller is pool contract", + prevRealm: std.NewCodeRealm(consts.POOL_PATH), + }, + { + name: "caller is router contract", + prevRealm: std.NewCodeRealm(consts.ROUTER_PATH), + }, + { + name: "caller is staker contract", + prevRealm: std.NewCodeRealm(consts.STAKER_PATH), + }, + { + name: "caller is stranger", + prevRealm: dummyRealm, + shouldPanic: true, + panicMsg: "[GNOSWAP-PROTOCOL_FEE-001] caller has no permission", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + std.TestSetRealm(test.prevRealm) + + if test.shouldPanic { + uassert.PanicsWithMessage(t, test.panicMsg, func() { + assertOnlyPoolRouterStaker() + }) + } else { + uassert.NotPanics(t, func() { + assertOnlyPoolRouterStaker() + }) + } + }) + } +} + +func TestAssertOnlyGovStaker(t *testing.T) { + tests := []struct { + name string + prevRealm std.Realm + shouldPanic bool + panicMsg string + }{ + { + name: "caller is gov/staker contract", + prevRealm: std.NewCodeRealm(consts.GOV_STAKER_PATH), + }, + { + name: "caller is stranger", + prevRealm: dummyRealm, + shouldPanic: true, + panicMsg: "[GNOSWAP-PROTOCOL_FEE-001] caller has no permission", + }, + } +} + +// helper +func transferToProtocolFee(t *testing.T, tokenPath string, amount uint64) { + t.Helper() + + std.TestSetRealm(adminRealm) + + tokenTeller := common.GetTokenTeller(tokenPath) + tokenTeller.Transfer(consts.PROTOCOL_FEE_ADDR, amount) + + tokenListWithAmount[tokenPath] += amount +} diff --git a/protocol_fee/utils.gno b/protocol_fee/utils.gno index ad8b9fb56..65a8b670a 100644 --- a/protocol_fee/utils.gno +++ b/protocol_fee/utils.gno @@ -2,13 +2,16 @@ package protocol_fee import ( "std" -) -func isUserCall() bool { - return std.PrevRealm().IsUser() -} + "gno.land/r/gnoswap/v1/common" +) func getPrev() (string, string) { prev := std.PrevRealm() return prev.Addr().String(), prev.PkgPath() } + +// assertOnlyNotHalted panics if the contract is halted. +func assertOnlyNotHalted() { + common.IsHalted() +} diff --git a/router/protocol_fee_swap.gno b/router/protocol_fee_swap.gno index 362620293..067a60ff2 100644 --- a/router/protocol_fee_swap.gno +++ b/router/protocol_fee_swap.gno @@ -9,6 +9,8 @@ import ( "gno.land/p/demo/ufmt" u256 "gno.land/p/gnoswap/uint256" + + pf "gno.land/r/gnoswap/v1/protocol_fee" ) const ( @@ -99,6 +101,7 @@ func handleSwapFee( outputTeller := common.GetTokenTeller(outputToken) outputTeller.TransferFrom(std.PrevRealm().Addr(), consts.PROTOCOL_FEE_ADDR, feeAmountUint64) + pf.AddToProtocolFee(outputToken, feeAmountUint64) prevAddr, prevRealm := getPrev() diff --git a/staker/protocol_fee_unstaking.gno b/staker/protocol_fee_unstaking.gno index 42b11eb25..82c04e688 100644 --- a/staker/protocol_fee_unstaking.gno +++ b/staker/protocol_fee_unstaking.gno @@ -49,6 +49,8 @@ func handleUnstakingFee( if internal { // staker contract has fee gns.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), feeAmount) + pf.AddToProtocolFee(consts.GNS_PATH, feeAmount) + std.Emit( "ProtocolFeeInternalReward", "prevAddr", prevAddr, @@ -62,6 +64,7 @@ func handleUnstakingFee( // external contract has fee teller := common.GetTokenTeller(tokenPath) teller.Transfer(consts.PROTOCOL_FEE_ADDR, feeAmount) + pf.AddToProtocolFee(tokenPath, feeAmount) std.Emit( "ProtocolFeeExternalReward",