Repository: gnoswap-labs/gnoswap
Pull request #562: refactor: protocol fee
Author: login: onlyhyde
Created: 2025-03-06T03:27:08Z | Merged: 2025-03-06T03:43:18Z

## Changes
1. Integration of duplicate code
2. Logic error Function modification
3. Modification of the logic so that the protocol fee function call in gov staker is correct
4. Test code modification
5. Addition of a getter function to check the distributed records


Diff:
diff --git a/contract/r/gnoswap/gov/staker/api_staker.gno b/contract/r/gnoswap/gov/staker/api_staker.gno
index 68e72a606b..1cb3122d56 100644
--- a/contract/r/gnoswap/gov/staker/api_staker.gno
+++ b/contract/r/gnoswap/gov/staker/api_staker.gno
@@ -47,6 +47,7 @@ func GetLockedInfoByAddress(addr std.Address) string {
 	return marshal(data)
 }
 
+// GetClaimableRewardByAddress gets the claimable reward of an address.
 func GetClaimableRewardByAddress(addr std.Address) string {
 	rewardState.finalize(collectAndGetGNSBalance(), updateAndGetProtocolFeeBalance())
 
diff --git a/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_test.gnoA b/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_test.gnoA
index 0552313364..ffd9ef8782 100644
--- a/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_test.gnoA
+++ b/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_test.gnoA
@@ -99,9 +99,13 @@ func mockProtocolFee(t *testing.T) {
 	std.TestSetOriginSend(std.Coins{{"ugnot", 10000}}, nil)
 	std.TestIssueCoins(consts.WUGNOT_ADDR, std.Coins{{"ugnot", 10000}})
 	wugnot.Deposit()
-
 	wugnot.Transfer(protocolFeeAddr, 10000)
 
+	std.TestSetRealm(stakerRealm)
+	pf.AddToProtocolFee(barPath, 1000)
+	pf.AddToProtocolFee(quxPath, 2500)
+	pf.AddToProtocolFee(wugnotPath, 10000)
+
 	uassert.Equal(t, bar.BalanceOf(protocolFeeAddr), uint64(1000))
 	uassert.Equal(t, bar.BalanceOf(devOpsAddr), uint64(0))
 	uassert.Equal(t, bar.BalanceOf(govStakerAddr), uint64(0))
diff --git a/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_with_launchpad_test.gnoA b/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_with_launchpad_test.gnoA
index becaf43375..016db0731c 100644
--- a/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_with_launchpad_test.gnoA
+++ b/contract/r/gnoswap/gov/staker/tests/governance_reward_protocol_fee_with_launchpad_test.gnoA
@@ -15,6 +15,7 @@ import (
 	"gno.land/r/gnoswap/v1/gns"
 	"gno.land/r/gnoswap/v1/gov/xgns"
 	lp "gno.land/r/gnoswap/v1/launchpad"
+	pf "gno.land/r/gnoswap/v1/protocol_fee"
 
 	// grc20 tokens
 	"gno.land/r/onbloc/bar"
@@ -43,7 +44,7 @@ func TestGovernanceRewardProtocolFeeWithLaunchpad(t *testing.T) {
 	delegateTestFromBobToBob(t)
 	launchPadCreateProject(t)
 	launchPadDepositTest(t)
-	//mockProtocolFee(t)
+	mockProtocolFeeTest(t)
 	//skipDummyBlocks(t)
 	//reward(t)
 }
@@ -935,26 +936,27 @@ func launchPadDepositTest(t *testing.T) {
 			"",
 		)
 		uassert.Equal(t, "gno.land/r/onbloc/obl:130:30:g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu:135", depositId)
+		uassert.Equal(t, `{"height":"135","now":"1234567914","depositId":"gno.land/r/onbloc/obl:130:30:g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu:135","projectId":"gno.land/r/onbloc/obl:130","tier":"30","depositor":"g1vfhkyh6lta047h6lta047h6lta047h6l03vdhu","amount":"1000000","depositHeight":"135","depositTime":"1234567914","depositCollectHeight":"0","depositCollectTime":"0","claimableHeight":"129735","claimableTime":"1234827114","claimableRewardAmount":"0","rewardCollected":"0","rewardCollectHeight":"0","rewardCollectTime":"0"}`,
+			lp.ApiGetDepositByDepositId(depositId))
 
 		std.TestSkipHeights(1)
 	})
 }
 
-//	func mockProtocolFee(t *testing.T) {
-//		// admin > protocol_fee
-//		// send qux, bar for testing
-//		std.TestSetRealm(adminRealm)
-//		bar.Transfer(protocolFeeAddr, 1000)
-//		qux.Transfer(protocolFeeAddr, 2500)
-//
-//		//uassert.Equal(t, bar.BalanceOf(protocolFeeAddr), uint64(1000))
-//		uassert.Equal(t, bar.BalanceOf(devOpsAddr), uint64(0))
-//		uassert.Equal(t, bar.BalanceOf(govStakerAddr), uint64(0))
-//
-//		//uassert.Equal(t, qux.BalanceOf(protocolFeeAddr), uint64(2500))
-//		uassert.Equal(t, qux.BalanceOf(devOpsAddr), uint64(0))
-//		uassert.Equal(t, qux.BalanceOf(govStakerAddr), uint64(0))
-//	}
+func mockProtocolFeeTest(t *testing.T) {
+	// admin > protocol_fee
+	// send qux, bar for testing
+	std.TestSetRealm(adminRealm)
+	bar.Transfer(protocolFeeAddr, 1000)
+	qux.Transfer(protocolFeeAddr, 2500)
+	std.TestSetRealm(stakerRealm)
+	pf.AddToProtocolFee(barPath, 1000)
+	pf.AddToProtocolFee(quxPath, 2500)
+	std.TestSkipHeights(1)
+
+	uassert.Equal(t, uint64(1000), bar.BalanceOf(protocolFeeAddr))
+	uassert.Equal(t, uint64(2500), qux.BalanceOf(protocolFeeAddr))
+}
 
 //func skipDummyBlocks(t *testing.T) {
 //	std.TestSkipHeights(10)
diff --git a/contract/r/gnoswap/launchpad/api_deposit.gno b/contract/r/gnoswap/launchpad/api_deposit.gno
index 847ceb05ec..415887cf79 100644
--- a/contract/r/gnoswap/launchpad/api_deposit.gno
+++ b/contract/r/gnoswap/launchpad/api_deposit.gno
@@ -17,24 +17,33 @@ func ApiGetClaimableDepositByAddress(address std.Address) uint64 {
 	for _, depositId := range depositsByUser[address] {
 		deposit := deposits[depositId]
 
-		project, exist := projects[deposit.projectId]
-		if !exist {
+		currentHeight := uint64(std.ChainHeight())
+		if (deposit.depositCollectHeight != 0) || (!deposit.isClaimable(currentHeight)) {
 			continue
 		}
 
-		tier := getTier(project, deposit.tier)
-		if checkTierActive(project, tier) {
+		project, err := getProject(deposit.projectId)
+		if err != nil {
 			continue
 		}
 
-		if deposit.depositCollectHeight != 0 {
+		tier, err := project.Tier(convertTierTypeStrToUint64(deposit.Tier()))
+		if err != nil {
+			continue
+		}
+		reward := tier.Reward()
+		rewardPerDeposit, err := reward.finalize(currentHeight, tier.ActualDepositAmount(), tier.TierAmountPerBlockX128())
+		if err != nil {
+			panic(err.Error())
+		}
+		reward.addRewardPerDeposit(rewardPerDeposit)
+		reward.setLastHeight(currentHeight)
+		rewardAmount := reward.deductReward(depositId, currentHeight)
+		if rewardAmount == 0 {
 			continue
 		}
 
-		rwd, _ := rewardStates.Get(deposit.projectId, deposit.tier)
-		reward := rwd.claim(depositId, uint64(std.ChainHeight()))
-
-		gnsToUser += reward
+		gnsToUser += rewardAmount
 	}
 
 	return gnsToUser
@@ -47,8 +56,7 @@ func ApiGetDepositByDepositId(depositId string) string {
 		return ""
 	}
 
-	builder := MetaBuilder().
-		WriteString("depositId", depositId)
+	builder := MetaBuilder()
 	DepositBuilder(builder, deposit)
 
 	return marshal(builder.Node())
@@ -76,8 +84,7 @@ func ApiGetDepositFullByDepositId(depositId string) string {
 		tier = project.tiers[180]
 	}
 
-	builder := MetaBuilder().
-		WriteString("depositId", depositId)
+	builder := MetaBuilder()
 
 	// Add project info
 	ProjectBuilder(builder, project)
diff --git a/contract/r/gnoswap/launchpad/json_builder.gno b/contract/r/gnoswap/launchpad/json_builder.gno
index 442fed7ded..c2eb60bd2f 100644
--- a/contract/r/gnoswap/launchpad/json_builder.gno
+++ b/contract/r/gnoswap/launchpad/json_builder.gno
@@ -87,6 +87,7 @@ func ProjectBuilder(b *json.NodeBuilder, project Project) *json.NodeBuilder {
 // DepositBuilder adds Deposit fields to JSON
 func DepositBuilder(b *json.NodeBuilder, deposit Deposit) *json.NodeBuilder {
 	return b.
+		WriteString("depositId", deposit.id).
 		WriteString("projectId", deposit.projectId).
 		WriteString("tier", deposit.tier).
 		WriteString("depositor", deposit.depositor.String()).
@@ -97,6 +98,7 @@ func DepositBuilder(b *json.NodeBuilder, deposit Deposit) *json.NodeBuilder {
 		WriteString("depositCollectTime", ufmt.Sprintf("%d", deposit.depositCollectTime)).
 		WriteString("claimableHeight", ufmt.Sprintf("%d", deposit.claimableHeight)).
 		WriteString("claimableTime", ufmt.Sprintf("%d", deposit.claimableTime)).
+		WriteString("claimableRewardAmount", ufmt.Sprintf("%d", deposit.rewardAmount)).
 		WriteString("rewardCollected", ufmt.Sprintf("%d", deposit.rewardCollected)).
 		WriteString("rewardCollectHeight", ufmt.Sprintf("%d", deposit.rewardCollectHeight)).
 		WriteString("rewardCollectTime", ufmt.Sprintf("%d", deposit.rewardCollectTime))
diff --git a/contract/r/gnoswap/protocol_fee/api.gno b/contract/r/gnoswap/protocol_fee/api.gno
new file mode 100644
index 0000000000..01f5889e50
--- /dev/null
+++ b/contract/r/gnoswap/protocol_fee/api.gno
@@ -0,0 +1,75 @@
+package protocol_fee
+
+import (
+	"std"
+	"strconv"
+	"time"
+
+	"gno.land/p/demo/avl"
+	"gno.land/p/demo/json"
+)
+
+func ApiGetAccuTransferToGovStaker() string {
+	distributedAmountList := GetAccuTransferToGovStaker()
+	if distributedAmountList == nil {
+		return ""
+	}
+
+	return marshal(buildByAvlTree(distributedAmountList))
+}
+
+func ApiGetAccuTransferToDevOps() string {
+	distributedAmountList := GetAccuTransferToDevOps()
+	if distributedAmountList == nil {
+		return ""
+	}
+
+	return marshal(buildByAvlTree(distributedAmountList))
+}
+
+func ApiGetHistoryTransferToGovStaker() string {
+	historyTransferList := GetHistoryOfDistributedToGovStaker()
+	if historyTransferList == nil {
+		return ""
+	}
+
+	return marshal(buildByAvlTree(historyTransferList))
+}
+
+func ApiGetHistoryTransferToDevOps() string {
+	historyTransferList := GetHistoryOfDistributedToDevOps()
+	if historyTransferList == nil {
+		return ""
+	}
+	return marshal(buildByAvlTree(historyTransferList))
+}
+
+func buildByAvlTree(tree *avl.Tree) *json.Node {
+	data := json.Builder().
+		WriteString("height", formatInt(std.ChainHeight())).
+		WriteString("now", formatInt(time.Now().Unix()))
+
+	tree.Iterate("", "", func(key string, value interface{}) bool {
+		data.WriteString(key, formatUint(value.(uint64)))
+		return false
+	})
+
+	return data.Node()
+}
+
+func formatUint(v uint64) string {
+	return strconv.FormatUint(v, 10)
+}
+
+func formatInt(v int64) string {
+	return strconv.FormatInt(v, 10)
+}
+
+func marshal(node *json.Node) string {
+	b, err := json.Marshal(node)
+	if err != nil {
+		panic(err.Error())
+	}
+
+	return string(b)
+}
diff --git a/contract/r/gnoswap/protocol_fee/api_test.gno b/contract/r/gnoswap/protocol_fee/api_test.gno
new file mode 100644
index 0000000000..e9e707c6a1
--- /dev/null
+++ b/contract/r/gnoswap/protocol_fee/api_test.gno
@@ -0,0 +1,65 @@
+package protocol_fee
+
+import (
+	"std"
+	"strconv"
+	"testing"
+	"time"
+
+	"gno.land/p/demo/avl"
+	"gno.land/p/demo/json"
+	"gno.land/p/demo/uassert"
+)
+
+func TestApiGetAccuTransferToGovStaker(t *testing.T) {
+	addAccuToGovStaker("gno.land/r/onbloc/bar", 1000)
+	addAccuToGovStaker("gno.land/r/onbloc/qux", 2000)
+
+	result := ApiGetAccuTransferToGovStaker()
+	uassert.Equal(t, `{"height":"123","now":"1234567890","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, result)
+
+	std.TestSetRealm(std.NewCodeRealm("gno.land/r/gnoswap/v1/gov/staker"))
+	ClearAccuTransferToGovStaker()
+	result = ApiGetAccuTransferToGovStaker()
+	uassert.Equal(t, `{"height":"123","now":"1234567890"}`, result)
+}
+
+func TestApiGetHistoryTransferToGovStaker(t *testing.T) {
+	std.TestSkipHeights(1)
+	addAccuToGovStaker("gno.land/r/onbloc/bar", 1000)
+	addAccuToGovStaker("gno.land/r/onbloc/qux", 2000)
+	updateDistributedToGovStakerHistory("gno.land/r/onbloc/bar", 1000)
+	updateDistributedToGovStakerHistory("gno.land/r/onbloc/qux", 2000)
+	resultHistory := ApiGetHistoryTransferToGovStaker()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, resultHistory)
+	resultAccu := ApiGetAccuTransferToGovStaker()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, resultAccu)
+
+	std.TestSetRealm(std.NewCodeRealm("gno.land/r/gnoswap/v1/gov/staker"))
+	ClearAccuTransferToGovStaker()
+	resultHistory = ApiGetHistoryTransferToGovStaker()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, resultHistory)
+	resultAccu = ApiGetAccuTransferToGovStaker()
+	uassert.Equal(t, `{"height":"124","now":"1234567892"}`, resultAccu)
+}
+
+func TestApiGetAccuTransferToDevOps(t *testing.T) {
+	std.TestSkipHeights(1)
+	addAccuToDevOps("gno.land/r/onbloc/bar", 1000)
+	addAccuToDevOps("gno.land/r/onbloc/qux", 2000)
+
+	result := ApiGetAccuTransferToDevOps()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, result)
+}
+
+func TestApiGetHistoryTransferToDevOps(t *testing.T) {
+	std.TestSkipHeights(1)
+	addAccuToDevOps("gno.land/r/onbloc/bar", 1000)
+	addAccuToDevOps("gno.land/r/onbloc/qux", 2000)
+	updateDistributedToDevOpsHistory("gno.land/r/onbloc/bar", 1000)
+	updateDistributedToDevOpsHistory("gno.land/r/onbloc/qux", 2000)
+	resultHistory := ApiGetHistoryTransferToDevOps()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"1000","gno.land/r/onbloc/qux":"2000"}`, resultHistory)
+	resultAccu := ApiGetAccuTransferToDevOps()
+	uassert.Equal(t, `{"height":"124","now":"1234567892","gno.land/r/onbloc/bar":"2000","gno.land/r/onbloc/qux":"4000"}`, resultAccu)
+}
diff --git a/contract/r/gnoswap/protocol_fee/getter.gno b/contract/r/gnoswap/protocol_fee/getter.gno
new file mode 100644
index 0000000000..4d14f99a62
--- /dev/null
+++ b/contract/r/gnoswap/protocol_fee/getter.gno
@@ -0,0 +1,65 @@
+package protocol_fee
+
+import (
+	"gno.land/p/demo/avl"
+)
+
+// GetAccuTransferToGovStaker returns the accuToGovStaker.
+func GetAccuTransferToGovStaker() *avl.Tree {
+	return accuToGovStaker
+}
+
+// GetAccuTransferToDevOps returns the accuToDevOps.
+func GetAccuTransferToDevOps() *avl.Tree {
+	return accuToDevOps
+}
+
+// GetAccuTransferToGovStakerByTokenPath returns the accumulated transfer to gov/staker by token path.
+func GetAccuTransferToGovStakerByTokenPath(path string) uint64 {
+	amountI, exists := accuToGovStaker.Get(path)
+	if !exists {
+		return 0
+	}
+
+	return amountI.(uint64)
+}
+
+// GetAccuTransferToDevOpsByTokenPath returns the accumulated transfer to devOps by token path.
+func GetAccuTransferToDevOpsByTokenPath(path string) uint64 {
+	amountI, exists := accuToDevOps.Get(path)
+	if !exists {
+		return 0
+	}
+
+	return amountI.(uint64)
+}
+
+// GetHistoryOfDistributedToGovStaker returns the history of distributed to gov/staker.
+func GetHistoryOfDistributedToGovStaker() *avl.Tree {
+	return distributedToGovStakerHistory
+}
+
+// GetHistoryOfDistributedToDevOps returns the history of distributed to devOps.
+func GetHistoryOfDistributedToDevOps() *avl.Tree {
+	return distributedToDevOpsHistory
+}
+
+// GetHistoryOfDistributedToGovStakerByTokenPath returns the history of distributed to gov/staker by token path.
+func GetHistoryOfDistributedToGovStakerByTokenPath(path string) uint64 {
+	amountI, exists := distributedToGovStakerHistory.Get(path)
+	if !exists {
+		return 0
+	}
+
+	return amountI.(uint64)
+}
+
+// GetHistoryOfDistributedToDevOpsByTokenPath returns the history of distributed to devOps by token path.
+func GetHistoryOfDistributedToDevOpsByTokenPath(path string) uint64 {
+	amountI, exists := distributedToDevOpsHistory.Get(path)
+	if !exists {
+		return 0
+	}
+
+	return amountI.(uint64)
+}
diff --git a/contract/r/gnoswap/protocol_fee/protocol_fee.gno b/contract/r/gnoswap/protocol_fee/protocol_fee.gno
index 38aef2f543..f003ed6d7a 100644
--- a/contract/r/gnoswap/protocol_fee/protocol_fee.gno
+++ b/contract/r/gnoswap/protocol_fee/protocol_fee.gno
@@ -3,6 +3,7 @@ package protocol_fee
 import (
 	"std"
 	"strconv"
+	"strings"
 
 	"gno.land/p/demo/avl"
 	"gno.land/p/demo/ufmt"
@@ -20,20 +21,25 @@ var (
 	// by default, devOps will get 0% of the protocol fee (which means gov/staker will get 100% of the protocol fee)
 	devOpsPct uint64 = 0 // 0%
 
-	// gns amount distributed to devOps and gov/staker
-	gnsToDevOps    uint64
-	gnsToGovStaker uint64
-
 	// accumulated amount distributed to gov/staker by token path
 	accuToGovStaker = avl.NewTree()
+	accuToDevOps    = avl.NewTree()
+
+	// distributedToDevOpsHistory and distributedToGovStakerHistory are used to keep track of the distribution history
+	distributedToGovStakerHistory = avl.NewTree()
+	distributedToDevOpsHistory    = avl.NewTree()
 
 	protocolFeeAddr, _ = access.GetAddress(access.ROLE_PROTOCOL_FEE)
 	govStakerAddr, _   = access.GetAddress(access.ROLE_GOV_STAKER)
 	devOpsAddr, _      = access.GetAddress(access.ROLE_DEVOPS)
+
+	tokenListWithAmount = make(map[string]uint64) // tokenPath -> amount
 )
 
 // DistributeProtocolFee distributes the protocol fee to devOps and gov/staker.
-func DistributeProtocolFee() {
+// only `gov/staker` can execute this function.
+// It returns list of token with amount has been sent to gov/staker.
+func DistributeProtocolFee() map[string]uint64 {
 	currentLevel := halt.GetCurrentHaltLevel()
 	if currentLevel != halt.LvMainnetSafeMode {
 		// Check if withdrawals are specifically halted, not just if the system is halted
@@ -46,38 +52,59 @@ func DistributeProtocolFee() {
 		}
 	}
 
-	tokens := common.ListRegisteredTokens()
-	if len(tokens) == 0 {
-		return
-	}
-
-	for _, token := range tokens {
-		// default distribute protocol fee percent
-		// govStaker 100%
-		// ...
+	assertOnlyGovStaker()
+	sentToDevOpsForEvent := make([]string, 0)
+	sentToGovStakerForEvent := make([]string, 0)
+	toReturnDistributedToGovStaker := make(map[string]uint64)
 
+	for token, amount := range tokenListWithAmount {
 		balance := common.BalanceOf(token, protocolFeeAddr)
-		if balance > 0 {
-			toDevOps := balance * devOpsPct / 10000 // default 0%
-			toGovStaker := balance - toDevOps       // default 100%
 
-			if token == consts.GNS_PATH {
-				gnsToDevOps = toDevOps
-				gnsToGovStaker = toGovStaker
-			}
+		// 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),
+			))
+		}
 
-			addAccuToGovStaker(token, toGovStaker)
+		if amount > 0 {
+			toDevOpsAmount := balance * devOpsPct / 10000 // default 0%
+			toGovStakerAmount := balance - toDevOpsAmount // default 100%
 
 			tokenTeller := common.GetTokenTeller(token)
-			if toDevOps > 0 {
-				tokenTeller.Transfer(devOpsAddr, toDevOps)
+			if toDevOpsAmount > 0 {
+				addAccuToDevOps(token, toDevOpsAmount)
+				updateDistributedToDevOpsHistory(token, toDevOpsAmount)
+				sentToDevOpsForEvent = append(sentToDevOpsForEvent, makeEventString(token, toDevOpsAmount))
+				tokenTeller.Transfer(devOpsAddr, toDevOpsAmount)
 			}
 
-			if toGovStaker > 0 {
-				tokenTeller.Transfer(govStakerAddr, toGovStaker)
+			if toGovStakerAmount > 0 {
+				addAccuToGovStaker(token, toGovStakerAmount)
+				updateDistributedToGovStakerHistory(token, toGovStakerAmount)
+				sentToGovStakerForEvent = append(sentToGovStakerForEvent, makeEventString(token, toGovStakerAmount))
+				tokenTeller.Transfer(govStakerAddr, toGovStakerAmount)
+
+				toReturnDistributedToGovStaker[token] = toGovStakerAmount
 			}
 		}
 	}
+
+	clearTokenListWithAmount()
+
+	prevAddr, prevRealm := getPrev()
+	std.Emit(
+		"TransferProtocolFee",
+		"prevAddr", prevAddr,
+		"prevRealm", prevRealm,
+		"toDevOps", strings.Join(sentToDevOpsForEvent, ","),
+		"toGovStaker", strings.Join(sentToGovStakerForEvent, ","),
+	)
+
+	return toReturnDistributedToGovStaker
 }
 
 func GetDevOpsPct() uint64 {
@@ -127,24 +154,18 @@ func setDevOpsPct(pct uint64) {
 	)
 }
 
-// GetLastTransferToDevOps returns the last transfer to devOps.
-func GetLastTransferToDevOps() uint64 {
-	return gnsToDevOps
-}
-
-// GetAccuTransferToGovStaker returns the accuToGovStaker.
-func GetAccuTransferToGovStaker() *avl.Tree {
-	return accuToGovStaker
+// 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
 }
 
-// GetAccuTransferToGovStakerByTokenPath returns the accumulated transfer to gov/staker by token path.
-func GetAccuTransferToGovStakerByTokenPath(path string) uint64 {
-	amountI, exists := accuToGovStaker.Get(path)
-	if !exists {
-		return 0
-	}
-
-	return amountI.(uint64)
+// ClearTokenListWithAmount clears the tokenListWithAmount.
+// only `gov/staker` can execute this function.
+func ClearTokenListWithAmount() {
+	assertOnlyGovStaker()
+	clearTokenListWithAmount()
 }
 
 // ClearAccuTransferToGovStaker clears the accuToGovStaker.
@@ -163,9 +184,75 @@ func ClearAccuTransferToGovStaker() {
 	accuToGovStaker = avl.NewTree()
 }
 
+// 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
+}
+
 // addAccuToGovStaker adds the amount to the accuToGovStaker by token path.
-func addAccuToGovStaker(path string, amount uint64) {
-	before := GetAccuTransferToGovStakerByTokenPath(path)
+func addAccuToGovStaker(tokenPath string, amount uint64) {
+	before := GetAccuTransferToGovStakerByTokenPath(tokenPath)
 	after := before + amount
-	accuToGovStaker.Set(path, after)
+	accuToGovStaker.Set(tokenPath, after)
+}
+
+// addAccuToDevOps adds the amount to the accuToDevOps by token path.
+func addAccuToDevOps(tokenPath string, amount uint64) {
+	before := GetAccuTransferToDevOpsByTokenPath(tokenPath)
+	after := before + amount
+	accuToDevOps.Set(tokenPath, after)
+}
+
+// updateDistributedToGovStakerHistory updates the distributedToGovStakerHistory.
+func updateDistributedToGovStakerHistory(tokenPath string, amount uint64) {
+	// get the previous amount
+	prevAmountI, exists := distributedToGovStakerHistory.Get(tokenPath)
+	prevAmount := uint64(0)
+	if exists {
+		prevAmount = prevAmountI.(uint64)
+	}
+
+	// update the amount
+	afterAmount := prevAmount + amount
+	distributedToGovStakerHistory.Set(tokenPath, afterAmount)
+}
+
+// updateDistributedToDevOpsHistory updates the distributedToDevOpsHistory.
+func updateDistributedToDevOpsHistory(tokenPath string, amount uint64) {
+	// get the previous amount
+	prevAmountI, exists := distributedToDevOpsHistory.Get(tokenPath)
+	prevAmount := uint64(0)
+	if exists {
+		prevAmount = prevAmountI.(uint64)
+	}
+
+	// update the amount
+	afterAmount := prevAmount + amount
+	distributedToDevOpsHistory.Set(tokenPath, afterAmount)
+}
+
+// clearTokenListWithAmount clears the tokenListWithAmount.
+func clearTokenListWithAmount() {
+	tokenListWithAmount = map[string]uint64{}
+}
+
+func makeEventString(tokenPath string, amount uint64) string {
+	return tokenPath + "*FEE*" + strconv.FormatUint(amount, 10)
 }
diff --git a/contract/r/gnoswap/protocol_fee/protocol_fee_test.gno b/contract/r/gnoswap/protocol_fee/protocol_fee_test.gno
index 6f1054bb81..b537f76b9e 100644
--- a/contract/r/gnoswap/protocol_fee/protocol_fee_test.gno
+++ b/contract/r/gnoswap/protocol_fee/protocol_fee_test.gno
@@ -6,17 +6,23 @@ import (
 
 	"gno.land/p/demo/testutils"
 	"gno.land/p/demo/uassert"
+	"gno.land/p/gnoswap/consts"
+
+	"gno.land/r/gnoswap/v1/access"
+	"gno.land/r/gnoswap/v1/common"
+	pf "gno.land/r/gnoswap/v1/protocol_fee"
 
 	"gno.land/r/onbloc/bar"
+	_ "gno.land/r/onbloc/baz"
+	_ "gno.land/r/onbloc/foo"
 	"gno.land/r/onbloc/qux"
-
-	"gno.land/r/gnoswap/v1/access"
 )
 
 var (
 	adminAddr, _ = access.GetAddress(access.ROLE_ADMIN)
 	adminRealm   = std.NewUserRealm(adminAddr)
 	adminUser    = adminAddr
+	dummyRealm   = std.NewCodeRealm("gno.land/r/dummy")
 )
 
 func TestDistributeProtocolFee(t *testing.T) {
@@ -26,6 +32,13 @@ func TestDistributeProtocolFee(t *testing.T) {
 	bar.Transfer(protocolFeeAddr, 1000)
 	qux.Transfer(protocolFeeAddr, 1000)
 
+	stakerRealm := std.NewCodeRealm(consts.STAKER_PATH)
+	std.TestSetRealm(stakerRealm)
+	pf.AddToProtocolFee("gno.land/r/onbloc/bar", 1000)
+	pf.AddToProtocolFee("gno.land/r/onbloc/qux", 1000)
+
+	govStakerRealm := std.NewCodeRealm(consts.GOV_STAKER_PATH)
+	std.TestSetRealm(govStakerRealm)
 	uassert.Equal(t, bar.BalanceOf(protocolFeeAddr), uint64(1000))
 	uassert.Equal(t, bar.BalanceOf(devOpsAddr), uint64(0))
 	uassert.Equal(t, bar.BalanceOf(govStakerAddr), uint64(0))
@@ -75,3 +88,218 @@ func TestSetDevOpsPctByAdmin(t *testing.T) {
 	SetDevOpsPctByAdmin(123)
 	uassert.Equal(t, GetDevOpsPct(), uint64(123))
 }
+
+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", devOpsAddr)
+	devOpsOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", devOpsAddr)
+	govStkaerOldFoo := common.BalanceOf("gno.land/r/onbloc/foo", govStakerAddr)
+	govStkaerOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", govStakerAddr)
+	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 := DistributeProtocolFee()
+	// 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", devOpsAddr)
+	devOpsNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", devOpsAddr)
+	uassert.Equal(t, devOpsNewFoo, uint64(49))
+	uassert.Equal(t, devOpsNewBaz, uint64(98))
+
+	govStkaerNewFoo := common.BalanceOf("gno.land/r/onbloc/foo", govStakerAddr)
+	govStkaerNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", govStakerAddr)
+	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(protocolFeeAddr, amount)
+
+	tokenListWithAmount[tokenPath] += amount
+}
diff --git a/contract/r/gnoswap/protocol_fee/token_list_with_amount.gno b/contract/r/gnoswap/protocol_fee/token_list_with_amount.gno
deleted file mode 100644
index 5e79d0e4d6..0000000000
--- a/contract/r/gnoswap/protocol_fee/token_list_with_amount.gno
+++ /dev/null
@@ -1,134 +0,0 @@
-package protocol_fee
-
-import (
-	"std"
-	"strings"
-
-	"gno.land/p/demo/ufmt"
-
-	"gno.land/r/gnoswap/v1/common"
-)
-
-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, protocolFeeAddr)
-
-		// 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(devOpsAddr, toDevOps)
-				sentToDevOps = append(sentToDevOps, makeEventString(token, toDevOps))
-			}
-
-			if toGovStaker > 0 {
-				tokenTeller.Transfer(govStakerAddr, 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.PreviousRealm().Address()
-
-	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.PreviousRealm().Address()
-	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/contract/r/gnoswap/protocol_fee/token_list_with_amount_test.gno b/contract/r/gnoswap/protocol_fee/token_list_with_amount_test.gno
deleted file mode 100644
index 525ca91be7..0000000000
--- a/contract/r/gnoswap/protocol_fee/token_list_with_amount_test.gno
+++ /dev/null
@@ -1,231 +0,0 @@
-package protocol_fee
-
-import (
-	"std"
-	"testing"
-
-	"gno.land/p/demo/uassert"
-
-	"gno.land/p/gnoswap/consts"
-	"gno.land/r/gnoswap/v1/common"
-
-	_ "gno.land/r/onbloc/baz"
-	_ "gno.land/r/onbloc/foo"
-)
-
-var dummyRealm = std.NewCodeRealm("gno.land/r/dummy")
-
-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", devOpsAddr)
-	devOpsOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", devOpsAddr)
-	govStkaerOldFoo := common.BalanceOf("gno.land/r/onbloc/foo", govStakerAddr)
-	govStkaerOldBaz := common.BalanceOf("gno.land/r/onbloc/baz", govStakerAddr)
-	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", devOpsAddr)
-	devOpsNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", devOpsAddr)
-	uassert.Equal(t, devOpsNewFoo, uint64(49))
-	uassert.Equal(t, devOpsNewBaz, uint64(98))
-
-	govStkaerNewFoo := common.BalanceOf("gno.land/r/onbloc/foo", govStakerAddr)
-	govStkaerNewBaz := common.BalanceOf("gno.land/r/onbloc/baz", govStakerAddr)
-	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(protocolFeeAddr, amount)
-
-	tokenListWithAmount[tokenPath] += amount
-}
diff --git a/contract/r/gnoswap/protocol_fee/utils.gno b/contract/r/gnoswap/protocol_fee/utils.gno
index 5978776b71..46ee0e8002 100644
--- a/contract/r/gnoswap/protocol_fee/utils.gno
+++ b/contract/r/gnoswap/protocol_fee/utils.gno
@@ -4,6 +4,7 @@ import (
 	"std"
 
 	phalt "gno.land/p/gnoswap/halt"
+	"gno.land/r/gnoswap/v1/common"
 	"gno.land/r/gnoswap/v1/halt"
 )
 
@@ -23,3 +24,24 @@ func assertOnlyNotHalted() {
 		}
 	}
 }
+
+// assertOnlyPoolRouterStaker panics if the caller is not the pool, router, or staker contract.
+func assertOnlyPoolRouterStaker() {
+	caller := std.PreviousRealm().Address()
+
+	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.PreviousRealm().Address()
+	if err := common.GovStakerOnly(caller); err != nil {
+		panic(err.Error())
+	}
+}


Reviews:
user: login: notJoon
body: I left some modifications, but LGTM
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-06T03:34:24Z
user: login: notJoon
body: 👍 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T03:40:42Z

Review comments:
diff_hunk: @@ -46,38 +52,59 @@ func DistributeProtocolFee() {
 		}
 	}
 
-	tokens := common.ListRegisteredTokens()
-	if len(tokens) == 0 {
-		return
-	}
-
-	for _, token := range tokens {
-		// default distribute protocol fee percent
-		// govStaker 100%
-		// ...
+	assertOnlyGovStaker()
+	sentToDevOpsForEvent := []string{}
+	sentToGovStakerForEvent := []string{}
+	toReturnDistributedToGovStaker := map[string]uint64{}
path: contract/r/gnoswap/protocol_fee/protocol_fee.gno
user: login: notJoon
body: ```suggestion
	sentToDevOpsForEvent := make([]string, 0)
	sentToGovStakerForEvent := make([]string, 0)
	toReturnDistributedToGovStaker := make(map[string]uint64)
```
created_at: 2025-03-06T03:30:05Z
author_association: MEMBER
diff_hunk: @@ -163,9 +184,75 @@ func ClearAccuTransferToGovStaker() {
 	accuToGovStaker = avl.NewTree()
 }
 
+// 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
+}
+
 // addAccuToGovStaker adds the amount to the accuToGovStaker by token path.
-func addAccuToGovStaker(path string, amount uint64) {
-	before := GetAccuTransferToGovStakerByTokenPath(path)
+func addAccuToGovStaker(tokenPath string, amount uint64) {
+	before := GetAccuTransferToGovStakerByTokenPath(tokenPath)
 	after := before + amount
-	accuToGovStaker.Set(path, after)
+	accuToGovStaker.Set(tokenPath, after)
+}
+
+// addAccuToDevOps adds the amount to the accuToDevOps by token path.
+func addAccuToDevOps(tokenPath string, amount uint64) {
+	before := GetAccuTransferToDevOpsByTokenPath(tokenPath)
+	after := before + amount
+	accuToDevOps.Set(tokenPath, after)
+}
+
+// updateDistributedToGovStakerHistory updates the distributedToGovStakerHistory.
+func updateDistributedToGovStakerHistory(tokenPath string, amount uint64) {
+	// get the previous amount
+	prevAmountI, exists := distributedToGovStakerHistory.Get(tokenPath)
+	prevAmount := uint64(0)
+	if exists {
+		prevAmount = prevAmountI.(uint64)
+	}
+
+	// update the amount
+	afterAmount := prevAmount + amount
+	distributedToGovStakerHistory.Set(tokenPath, afterAmount)
+}
+
+// updateDistributedToDevOpsHistory updates the distributedToDevOpsHistory.
+func updateDistributedToDevOpsHistory(tokenPath string, amount uint64) {
+	// get the previous amount
+	prevAmountI, exists := distributedToDevOpsHistory.Get(tokenPath)
+	prevAmount := uint64(0)
+	if exists {
+		prevAmount = prevAmountI.(uint64)
+	}
+
+	// update the amount
+	afterAmount := prevAmount + amount
+	distributedToDevOpsHistory.Set(tokenPath, afterAmount)
+}
+
+// clearTokenListWithAmount clears the tokenListWithAmount.
+func clearTokenListWithAmount() {
+	tokenListWithAmount = map[string]uint64{}
+}
+
+func makeEventString(tokenPath string, amount uint64) string {
+	return tokenPath + "*FEE*" + ufmt.Sprintf("%d", amount)
path: contract/r/gnoswap/protocol_fee/protocol_fee.gno
user: login: notJoon
body: `strconv` has less overhead than `ufmt`

```suggestion
	return tokenPath + "*FEE*" + strconv.FormatUint(amount, 10)
```
created_at: 2025-03-06T03:32:50Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-06T03:35:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=gnoswap-labs_gnoswap&pullRequest=562) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=gnoswap-labs_gnoswap&pullRequest=562&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=gnoswap-labs_gnoswap&pullRequest=562&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=gnoswap-labs_gnoswap&pullRequest=562&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=gnoswap-labs_gnoswap&pullRequest=562&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=gnoswap-labs_gnoswap&pullRequest=562&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=gnoswap-labs_gnoswap&pullRequest=562)


author_association: NONE