Skip to content

fix(halt): allow emergency recovery settlement paths#1257

Open
junghoon-vans wants to merge 11 commits intomainfrom
GSW-2521-emergency-recovery-halt
Open

fix(halt): allow emergency recovery settlement paths#1257
junghoon-vans wants to merge 11 commits intomainfrom
GSW-2521-emergency-recovery-halt

Conversation

@junghoon-vans
Copy link
Copy Markdown
Member

@junghoon-vans junghoon-vans commented Apr 20, 2026

Summary

  • keep emergency recovery and settlement flows available where withdraw-mode recovery should still work, including position, staker, launchpad, gov-staker, and protocol-fee settlement paths
  • keep growth and admin-mutating operations blocked during emergency mode, and add scenario/unit coverage that locks in the intended halt matrix
  • clarify OrWithdraw panic messages so failures now explain that both the primary operation and withdraw fallback are blocked

Verification

  • make test PKG=gno.land/r/gnoswap/halt
  • make test PKG=gno.land/r/gnoswap/gnft
  • make test PKG=gno.land/r/gnoswap/protocol_fee/v1
  • make test PKG=gno.land/r/gnoswap/scenario/halt

Summary by CodeRabbit

  • New Features

    • Emergency halt now allows recovery actions: users can claim rewards, withdraw funds, and complete certain position/NFT operations to reclaim assets.
    • Protocol fees are buffered and processed asynchronously via a pending-fee queue for more resilient fee handling.
  • Tests

    • Added tests covering emergency-mode recovery and buffered protocol-fee behavior across rewards, launchpad, staker, position, pool, and protocol-fee flows.
  • Breaking Changes

    • Two protocol-fee reset/clear endpoints removed; adding protocol fees now surfaces errors on failure.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 20, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cb464e1-d96f-4b8b-9283-e3c30d945520

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Removed many operation-specific halt guards, added a deferred pending-protocol-fee buffering/collection mechanism, introduced protocol-fee distributed-history APIs and getters, updated protocol_fee AddToProtocolFee to return an error, and adjusted tests to reflect emergency-mode recovery semantics.

Changes

Cohort / File(s) Summary
Protocol-fee core & API
contract/r/gnoswap/protocol_fee/types.gno, contract/r/gnoswap/protocol_fee/proxy.gno, contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno, contract/r/gnoswap/protocol_fee/v1/errors.gno
AddToProtocolFee now returns error; removed ClearTokenListWithAmount/ClearAccuTransferToGovStaker; added errProtocolFeeHalted and refactored DistributeProtocolFee flow to use actual-distributed history and new error paths.
Protocol-fee state, history & getters
contract/r/gnoswap/protocol_fee/v1/*, .../protocol_fee/v1/protocol_fee_state.gno, .../protocol_fee/v1/getter.gno, .../protocol_fee/v1/init.gno
Added AVL-backed distributed history stores, new getters for actual-distributed amounts, bookCollectedProtocolFees/getOutstandingDistributionTokens helpers, and moved clearing/bookkeeping into new flows.
Pending protocol-fee buffering (router/pool/staker)
contract/r/gnoswap/router/v1/protocol_fee_swap.gno, contract/r/gnoswap/pool/v1/protocol_fee.gno, contract/r/gnoswap/staker/v1/protocol_fee_unstaking.gno
Replaced immediate transfer→pf.AddToProtocolFee with addPendingProtocolFee + tryCollectPendingProtocolFees flow that accumulates pending fees and attempts batched AddToProtocolFee calls (with approvals, deletions on success, early-return on AddToProtocolFee error).
Store keys, APIs & inits for pending fees
contract/r/gnoswap/*/store.gno, contract/r/gnoswap/*/types.gno, contract/r/gnoswap/*/v1/init.gno, contract/r/gnoswap/*/v1/_mock_test.gno, contract/r/gnoswap/test/fuzz/_mock_test.gno
Added pendingProtocolFees persistent key and accessors across router/pool/staker stores and interfaces; updated mock stores and init routines to create/initialize empty pending-fee maps.
Pool manager & router adjustments
contract/r/gnoswap/pool/v1/manager.gno, contract/r/gnoswap/router/v1/protocol_fee_swap.gno
Removed direct protocol_fee module dependency in some flows; CreatePool and swap-fee handling now queue pending fees and call tryCollectPendingProtocolFees before/after queuing.
Protocol-fee mocks, proxies, and upgrade fixtures
contract/r/gnoswap/protocol_fee/_mock_test.gno, contract/r/gnoswap/protocol_fee/proxy.gno, contract/r/scenario/upgrade/.../protocol_fee/test_impl.gno
Mocks updated: AddToProtocolFee returns error; removed deprecated Clear* methods; added actual-distributed getters; proxy and test-impls updated to new signatures and getters.
Halt helpers & tests
contract/r/gnoswap/halt/assert.gno, contract/r/gnoswap/halt/*_test.gno, contract/r/gnoswap/halt/config_test.gno
Added AssertIsNotHaltedProtocolFeeOrWithdraw() and tests validating combined halt semantics; adjusted emergency/complete expectations.
Widespread halt-assert removals / adjustments
contract/r/gnoswap/gnft/gnft.gno, .../community_pool/community_pool.gno, .../emission/emission.gno, .../gov/.../governance_propose.gno, .../launchpad/v1/*.gno, .../pool/v1/pool.gno, .../position/v1/*.gno, .../staker/v1/*.gno, .../gov/xgns/xgns.gno
Removed many operation-specific halt guards (position, launchpad, gov_staker, pool, staker, gnft, etc.); many functions now only assert withdraw halt or no longer assert the former halt; corresponding tests updated or removed.
Tests: emergency-recovery & expectations
multiple contract/r/gnoswap/*/*_test.gno, tests/integration/*
Adjusted test expectations from operation-specific halt panics to withdraw-related messages or no-panics under emergency; added tests asserting emergency-mode allows certain recovery/settlement paths; updated integration expectations.
Other supporting changes (mocks/stores/state consumers)
contract/r/gnoswap/mock/*, contract/r/gnoswap/gov/staker/v1/state.gno, contract/r/gnoswap/pool/*, contract/r/gnoswap/router/*
Added pending-fee fields/methods to various mocks, updated gov/staker to read actual-distributed amounts, and adapted state/consumers to new protocol-fee getters.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RouterV1
  participant Store
  participant ProtocolFee
  participant Token

  Client->>RouterV1: handleSwapFee(tokenPath, amount)
  RouterV1->>RouterV1: tryCollectPendingProtocolFees()
  RouterV1->>Store: GetPendingProtocolFees()
  alt pending exists and not halted
    RouterV1->>Token: SafeGRC20Approve(protocolFeeAddr, amount)
    RouterV1->>ProtocolFee: AddToProtocolFee(tokenPath, amount)
    ProtocolFee-->>RouterV1: (ok / error)
    RouterV1->>Store: SetPendingProtocolFees(updatedMap)
  else protocol fee halted or no pending
    RouterV1->>Store: addPendingProtocolFee(tokenPath, amount) (SetPendingProtocolFees)
  end
  RouterV1->>Client: return
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(halt): allow emergency recovery settlement paths' accurately describes the main objective of the changeset: enabling emergency recovery and settlement operations while halt is active.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch GSW-2521-emergency-recovery-halt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@junghoon-vans junghoon-vans self-assigned this Apr 20, 2026
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about removing halt from gnft?
It seems better for nft to function as a neutral resource rather than being included in the domain.

Can you share your thoughts? @dongwon8247

Copy link
Copy Markdown
Member

@jinoosss jinoosss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the domain and withdrawal functions should be completely separated.
However, if the protocol fee, which is called internally by the contract, is halted, it should not affect other functions.
For example, Router Swap, Collect Fee / Reward, etc.

In my opinion, we need to consider how to handle the halt of the AddToProtocolFee function in ProtocolFee.
When the protocol fee is halted, we may need to manage any pending amounts that have not yet been processed.

Comment thread contract/r/gnoswap/community_pool/community_pool.gno
Comment thread contract/r/gnoswap/pool/v1/pool.gno
Comment thread contract/r/gnoswap/position/v1/position.gno Outdated
// Note: Default split is 0% devOps, 100% gov/staker.
func (pf *protocolFeeV1) DistributeProtocolFee() map[string]int64 {
halt.AssertIsNotHaltedProtocolFee()
halt.AssertIsNotHaltedProtocolFeeOrWithdraw()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is not withdraw ?

Comment thread contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno Outdated
Comment thread contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contract/r/gnoswap/gov/xgns/xgns_test.gno (1)

391-403: ⚠️ Potential issue | 🟡 Minor

Reset the withdraw halt bit before asserting burn recovery.

Burn now gates on OpTypeWithdraw, but this setup only resets/enables OpTypeXGns. Please clear OpTypeWithdraw as part of this test setup so the “Burn still allowed” case is isolated from any prior halt state.

🧪 Proposed test isolation fix
 func resetHalt(t *testing.T) {
 	t.Helper()
 	if adminAddr, ok := access.GetAddress(prbac.ROLE_ADMIN.String()); ok {
 		testing.SetRealm(testing.NewUserRealm(adminAddr))
 		halt.SetOperationStatus(cross, halt.OpTypeXGns, false)
+		halt.SetOperationStatus(cross, halt.OpTypeWithdraw, false)
 	}
 }
contract/r/gnoswap/gov/staker/v1/staker_reward.gno (1)

166-175: ⚠️ Potential issue | 🟡 Minor

Update the halt-condition docs for the split gate.

Line 168 still documents withdrawal halt as the only halt condition, but Lines 171-175 now block additions on gov_staker and removals on withdraw.

Proposed doc fix
 // Panics:
 //   - if caller is not the launchpad contract
-//   - if system is halted for withdrawals
+//   - if add is true and gov_staker operations are halted
+//   - if add is false and withdrawals are halted
 //   - if access control operations fail
 func (gs *govStakerV1) SetAmountByProjectWallet(addr address, amount int64, add bool) {

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d4259040-e9ac-4b54-afde-154d7c980fa7

📥 Commits

Reviewing files that changed from the base of the PR and between 96b4b18 and 93e6e34.

⛔ Files ignored due to path filters (15)
  • contract/r/scenario/halt/emergency_mode_gov_staker_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_governance_propose_execute_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_launchpad_growth_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_launchpad_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_position_recovery_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_protocol_fee_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_staker_recovery_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_gov_staker_settlement_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_governance_and_withdraw_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_governance_propose_execute_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_launchpad_growth_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_launchpad_settlement_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_position_recovery_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_protocol_fee_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_staker_recovery_blocked_filetest.gno is excluded by !**/*filetest.gno
📒 Files selected for processing (31)
  • contract/r/gnoswap/community_pool/community_pool.gno
  • contract/r/gnoswap/community_pool/community_pool_test.gno
  • contract/r/gnoswap/emission/emission.gno
  • contract/r/gnoswap/gnft/_helper_test.gno
  • contract/r/gnoswap/gnft/gnft.gno
  • contract/r/gnoswap/gnft/gnft_test.gno
  • contract/r/gnoswap/gov/governance/v1/governance_propose.gno
  • contract/r/gnoswap/gov/staker/v1/staker_delegate.gno
  • contract/r/gnoswap/gov/staker/v1/staker_delegate_test.gno
  • contract/r/gnoswap/gov/staker/v1/staker_reward.gno
  • contract/r/gnoswap/gov/staker/v1/staker_reward_test.gno
  • contract/r/gnoswap/gov/xgns/xgns.gno
  • contract/r/gnoswap/gov/xgns/xgns_test.gno
  • contract/r/gnoswap/halt/assert.gno
  • contract/r/gnoswap/halt/assert_test.gno
  • contract/r/gnoswap/halt/config_test.gno
  • contract/r/gnoswap/halt/getters_test.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_protocol_fee.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_reward.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_reward_test.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_withdraw.gno
  • contract/r/gnoswap/pool/v1/pool.gno
  • contract/r/gnoswap/pool/v1/protocol_fee.gno
  • contract/r/gnoswap/position/v1/position.gno
  • contract/r/gnoswap/position/v1/position_test.gno
  • contract/r/gnoswap/position/v1/reposition.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee_test.gno
  • contract/r/gnoswap/staker/v1/external_incentive.gno
  • contract/r/gnoswap/staker/v1/staker.gno
  • contract/r/gnoswap/staker/v1/staker_test.gno
💤 Files with no reviewable changes (11)
  • contract/r/gnoswap/gov/governance/v1/governance_propose.gno
  • contract/r/gnoswap/gov/staker/v1/staker_delegate.gno
  • contract/r/gnoswap/pool/v1/protocol_fee.gno
  • contract/r/gnoswap/position/v1/reposition.gno
  • contract/r/gnoswap/emission/emission.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_withdraw.gno
  • contract/r/gnoswap/staker/v1/staker.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_reward.gno
  • contract/r/gnoswap/community_pool/community_pool.gno
  • contract/r/gnoswap/launchpad/v1/launchpad_protocol_fee.gno
  • contract/r/gnoswap/staker/v1/external_incentive.gno

Comment thread contract/r/gnoswap/gov/staker/v1/staker_delegate_test.gno
Comment thread contract/r/gnoswap/gov/staker/v1/staker_reward_test.gno
Comment thread contract/r/gnoswap/launchpad/v1/launchpad_reward_test.gno
Comment thread contract/r/gnoswap/position/v1/position_test.gno
Comment thread contract/r/gnoswap/staker/v1/staker_test.gno
@junghoon-vans junghoon-vans force-pushed the GSW-2521-emergency-recovery-halt branch from 7ab5163 to b6266fb Compare April 20, 2026 11:40
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
contract/r/gnoswap/protocol_fee/v1/protocol_fee_test.gno (1)

1245-1265: ⚠️ Potential issue | 🟡 Minor

Assert halted accrual leaves token balances unchanged.

This test verifies the accounting map stays zero, but it would still pass if AddToProtocolFee transferred tokens and then returned the halt error before recording them. Capture holder/protocol-fee balances before the call and assert both are unchanged.

Proposed test strengthening
 			holderRealm := testing.NewCodeRealm(poolPath)
 			fundAndApproveProtocolFee(holderRealm, "gno.land/r/onbloc/bar", 100)
 			testing.SetRealm(holderRealm)
+			holderBefore := common.BalanceOf("gno.land/r/onbloc/bar", holderRealm.Address())
+			protocolFeeBefore := common.BalanceOf("gno.land/r/onbloc/bar", protocolFeeAddr)
 
 			// When - Then
 			if tt.shouldPanic {
@@
 				uassert.NotPanics(t, func() {
 					func(cur realm) {
 						err := pf.AddToProtocolFee("gno.land/r/onbloc/bar", 100)
 						uassert.NotNil(t, err)
 					}(cross)
 				})
+				uassert.Equal(t, holderBefore, common.BalanceOf("gno.land/r/onbloc/bar", holderRealm.Address()))
+				uassert.Equal(t, protocolFeeBefore, common.BalanceOf("gno.land/r/onbloc/bar", protocolFeeAddr))
 				if tt.assertState != nil {
 					tt.assertState(t, pf)
 				}
🧹 Nitpick comments (1)
contract/r/gnoswap/pool/v1/init.gno (1)

69-74: Naming inconsistency across modules.

HasPendingProtocolFees() here differs from HasPendingProtocolFeesKey() (router) and HasPendingProtocolFeesStoreKey() (staker). Not a bug, but consider aligning the naming for consistency since these three modules implement the same pattern.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b75d3b4d-bc31-4148-acf3-03ff8c25a75c

📥 Commits

Reviewing files that changed from the base of the PR and between de029b3 and 5e47984.

⛔ Files ignored due to path filters (9)
  • contract/r/scenario/gov/staker/complex_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/launchpad_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/launchpad_protocol_fee_reward_with_blocked_withdraw_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/staker_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_launchpad_growth_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_protocol_fee_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/balance_amount_mismatch_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/distribute_protocol_fee_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/distribute_with_different_devops_pct_filetest.gno is excluded by !**/*filetest.gno
📒 Files selected for processing (29)
  • contract/r/gnoswap/gov/staker/v1/state.gno
  • contract/r/gnoswap/mock/pool_store.gno
  • contract/r/gnoswap/mock/router_store.gno
  • contract/r/gnoswap/pool/store.gno
  • contract/r/gnoswap/pool/types.gno
  • contract/r/gnoswap/pool/v1/init.gno
  • contract/r/gnoswap/pool/v1/manager.gno
  • contract/r/gnoswap/pool/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/_mock_test.gno
  • contract/r/gnoswap/protocol_fee/proxy.gno
  • contract/r/gnoswap/protocol_fee/types.gno
  • contract/r/gnoswap/protocol_fee/v1/_mock_test.gno
  • contract/r/gnoswap/protocol_fee/v1/errors.gno
  • contract/r/gnoswap/protocol_fee/v1/getter.gno
  • contract/r/gnoswap/protocol_fee/v1/init.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee_state.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee_test.gno
  • contract/r/gnoswap/router/store.gno
  • contract/r/gnoswap/router/types.gno
  • contract/r/gnoswap/router/v1/_mock_test.gno
  • contract/r/gnoswap/router/v1/init.gno
  • contract/r/gnoswap/router/v1/protocol_fee_swap.gno
  • contract/r/gnoswap/staker/store.gno
  • contract/r/gnoswap/staker/types.gno
  • contract/r/gnoswap/staker/v1/_mock_test.gno
  • contract/r/gnoswap/staker/v1/init.gno
  • contract/r/gnoswap/staker/v1/protocol_fee_unstaking.gno
  • contract/r/gnoswap/staker/v1/protocol_fee_unstaking_test.gno
✅ Files skipped from review due to trivial changes (1)
  • contract/r/gnoswap/protocol_fee/v1/errors.gno
🚧 Files skipped from review as they are similar to previous changes (3)
  • contract/r/gnoswap/protocol_fee/proxy.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/types.gno

Comment thread contract/r/gnoswap/pool/v1/protocol_fee.gno
Comment on lines 65 to +84
func (pfs *protocolFeeState) distributeToDevOps(token string, amount int64) error {
if err := pfs.addAccuToDevOps(token, amount); err != nil {
return err
}

devOpsAddr := access.MustGetAddress(prabc.ROLE_DEVOPS.String())
common.SafeGRC20Transfer(cross, token, devOpsAddr, amount)

if err := pfs.addActualDistributedToDevOps(token, amount); err != nil {
return err
}

return nil
}

// distributeToGovStaker distributes tokens to Gov/Staker and updates related state.
// Amount should be greater than 0 (already checked in DistributeProtocolFee).
func (pfs *protocolFeeState) distributeToGovStaker(token string, amount int64) error {
if err := pfs.addAccuToGovStaker(token, amount); err != nil {
return err
}

govStakerAddr := access.MustGetAddress(prabc.ROLE_GOV_STAKER.String())
common.SafeGRC20Transfer(cross, token, govStakerAddr, amount)

if err := pfs.addActualDistributedToGovStaker(token, amount); err != nil {
return err
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move actual-distribution state updates before token transfers.

Lines 67 and 80 perform external token transfers before Lines 69 and 82 persist the new actual-distributed counters. If the store update returns an error after a successful transfer, the token movement can be completed without corresponding accounting. Update history first; a transfer panic should roll the transaction back.

As per coding guidelines, all state updates must occur before token transfers (Checks-Effects-Interactions pattern).

Proposed fix
 func (pfs *protocolFeeState) distributeToDevOps(token string, amount int64) error {
 	devOpsAddr := access.MustGetAddress(prabc.ROLE_DEVOPS.String())
-	common.SafeGRC20Transfer(cross, token, devOpsAddr, amount)
 
 	if err := pfs.addActualDistributedToDevOps(token, amount); err != nil {
 		return err
 	}
 
+	common.SafeGRC20Transfer(cross, token, devOpsAddr, amount)
+
 	return nil
 }
@@
 func (pfs *protocolFeeState) distributeToGovStaker(token string, amount int64) error {
 	govStakerAddr := access.MustGetAddress(prabc.ROLE_GOV_STAKER.String())
-	common.SafeGRC20Transfer(cross, token, govStakerAddr, amount)
 
 	if err := pfs.addActualDistributedToGovStaker(token, amount); err != nil {
 		return err
 	}
 
+	common.SafeGRC20Transfer(cross, token, govStakerAddr, amount)
+
 	return nil
 }

Comment thread contract/r/gnoswap/router/v1/protocol_fee_swap.gno
Comment on lines +71 to +84
for tokenPath, amount := range pendingProtocolFees {
if amount <= 0 {
delete(pendingProtocolFees, tokenPath)
continue
}

common.SafeGRC20Approve(cross, tokenPath, protocolFeeAddr, amount)
if err := pf.AddToProtocolFee(cross, tokenPath, amount); err != nil {
return
}
delete(pendingProtocolFees, tokenPath)
}

if err := s.store.SetPendingProtocolFees(pendingProtocolFees); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Persist each successful pending-fee collection before a later error can discard it.

If one pending token is collected successfully and a later pf.AddToProtocolFee returns an error, Line 79 returns before Line 84 persists the cloned map. The already-collected token remains pending in storage and can be collected again on the next retry.

🐛 Proposed fix: persist progress per entry and restore only the failed entry
 	for tokenPath, amount := range pendingProtocolFees {
 		if amount <= 0 {
 			delete(pendingProtocolFees, tokenPath)
 			continue
 		}
 
+		delete(pendingProtocolFees, tokenPath)
+		if err := s.store.SetPendingProtocolFees(pendingProtocolFees); err != nil {
+			panic(err)
+		}
+
 		common.SafeGRC20Approve(cross, tokenPath, protocolFeeAddr, amount)
 		if err := pf.AddToProtocolFee(cross, tokenPath, amount); err != nil {
+			pendingProtocolFees[tokenPath] = safeAddInt64(pendingProtocolFees[tokenPath], amount)
+			if setErr := s.store.SetPendingProtocolFees(pendingProtocolFees); setErr != nil {
+				panic(setErr)
+			}
 			return
 		}
-		delete(pendingProtocolFees, tokenPath)
 	}

@junghoon-vans junghoon-vans force-pushed the GSW-2521-emergency-recovery-halt branch from 5e47984 to 0775780 Compare April 21, 2026 00:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (4)
contract/r/gnoswap/protocol_fee/v1/protocol_fee_state.gno (1)

65-84: ⚠️ Potential issue | 🟠 Major

Move actual-distribution accounting before token transfers.

SafeGRC20Transfer executes before addActualDistributedTo*; if the later store update returns an error, tokens can move without matching distributed-history accounting. Keep effects before interactions. As per coding guidelines, all state updates must occur before token transfers (Checks-Effects-Interactions pattern).

Proposed fix
 func (pfs *protocolFeeState) distributeToDevOps(token string, amount int64) error {
 	devOpsAddr := access.MustGetAddress(prabc.ROLE_DEVOPS.String())
-	common.SafeGRC20Transfer(cross, token, devOpsAddr, amount)
 
 	if err := pfs.addActualDistributedToDevOps(token, amount); err != nil {
 		return err
 	}
 
+	common.SafeGRC20Transfer(cross, token, devOpsAddr, amount)
+
 	return nil
 }
@@
 func (pfs *protocolFeeState) distributeToGovStaker(token string, amount int64) error {
 	govStakerAddr := access.MustGetAddress(prabc.ROLE_GOV_STAKER.String())
-	common.SafeGRC20Transfer(cross, token, govStakerAddr, amount)
 
 	if err := pfs.addActualDistributedToGovStaker(token, amount); err != nil {
 		return err
 	}
 
+	common.SafeGRC20Transfer(cross, token, govStakerAddr, amount)
+
 	return nil
 }
contract/r/gnoswap/staker/v1/protocol_fee_unstaking.gno (1)

64-87: ⚠️ Potential issue | 🔴 Critical

Persist partial collection progress before returning on an error.

If one token is collected and deleted from the cloned map, then a later AddToProtocolFee returns an error, the early return skips SetPendingProtocolFees; the already-collected token remains pending in storage and can be collected again. Based on learnings, every fee transfer must be tracked correctly through AddToProtocolFee.

Proposed fix
 		common.SafeGRC20Approve(cross, tokenPath, protocolFeeAddr, amount)
 		if err := pf.AddToProtocolFee(cross, tokenPath, amount); err != nil {
+			if setErr := s.store.SetPendingProtocolFees(pendingProtocolFees); setErr != nil {
+				panic(setErr)
+			}
 			return
 		}
 		delete(pendingProtocolFees, tokenPath)
 	}
contract/r/gnoswap/pool/v1/protocol_fee.gno (1)

80-103: ⚠️ Potential issue | 🔴 Critical

Persist partial collection progress before returning on AddToProtocolFee errors.

A successful earlier entry is deleted only from the cloned map. If a later token returns an error, Line 95 exits before Line 100 persists those deletions, so the already-collected fees can be retried and double-collected. Based on learnings, every fee transfer must be tracked correctly through AddToProtocolFee.

Proposed fix
 		common.SafeGRC20Approve(cross, tokenPath, protocolFeeAddr, amount)
 		if err := pf.AddToProtocolFee(cross, tokenPath, amount); err != nil {
+			if setErr := i.store.SetPendingProtocolFees(pendingProtocolFees); setErr != nil {
+				panic(setErr)
+			}
 			return
 		}
 		delete(pendingProtocolFees, tokenPath)
 	}
contract/r/gnoswap/router/v1/protocol_fee_swap.gno (1)

123-136: ⚠️ Potential issue | 🔴 Critical

Persist per-token collection progress before external calls.

If a later pf.AddToProtocolFee returns an error at Line 130, earlier successful deletes only exist in the local clone and Line 136 is skipped, so already-collected fees can be retried and collected again. Persist the deletion before approval/collection, and restore the failed token only when AddToProtocolFee returns an error. As per coding guidelines, all state updates must occur before token transfers (Checks-Effects-Interactions pattern).

🐛 Proposed fix
 	for tokenPath, amount := range pendingProtocolFees {
 		if amount <= 0 {
 			delete(pendingProtocolFees, tokenPath)
 			continue
 		}
 
+		delete(pendingProtocolFees, tokenPath)
+		if err := r.store.SetPendingProtocolFees(pendingProtocolFees); err != nil {
+			panic(err)
+		}
+
 		common.SafeGRC20Approve(cross, tokenPath, protocolFeeAddr, amount)
 		if err := pf.AddToProtocolFee(cross, tokenPath, amount); err != nil {
+			pendingProtocolFees[tokenPath] = safeAddInt64(pendingProtocolFees[tokenPath], amount)
+			if setErr := r.store.SetPendingProtocolFees(pendingProtocolFees); setErr != nil {
+				panic(setErr)
+			}
 			return
 		}
-		delete(pendingProtocolFees, tokenPath)
 	}

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 34907148-de06-43ea-ad37-244fb7531dcb

📥 Commits

Reviewing files that changed from the base of the PR and between 5e47984 and 0775780.

⛔ Files ignored due to path filters (17)
  • contract/r/scenario/gov/staker/complex_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/launchpad_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/launchpad_protocol_fee_reward_with_blocked_withdraw_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/gov/staker/staker_protocol_fee_reward_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_gov_staker_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_launchpad_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/emergency_mode_protocol_fee_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_gov_staker_settlement_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_launchpad_growth_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_launchpad_settlement_blocked_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/halt/safe_mode_protocol_fee_settlement_allowed_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/launchpad/launchpad_deposit_project_multi_recipient_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/launchpad/launchpad_deposit_project_single_recipient_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/launchpad/launchpad_single_deposit_protocol_fee_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/balance_amount_mismatch_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/distribute_protocol_fee_filetest.gno is excluded by !**/*filetest.gno
  • contract/r/scenario/protocol_fee/distribute_with_different_devops_pct_filetest.gno is excluded by !**/*filetest.gno
📒 Files selected for processing (35)
  • contract/r/gnoswap/gov/staker/v1/state.gno
  • contract/r/gnoswap/mock/pool_store.gno
  • contract/r/gnoswap/mock/router_store.gno
  • contract/r/gnoswap/pool/store.gno
  • contract/r/gnoswap/pool/types.gno
  • contract/r/gnoswap/pool/v1/init.gno
  • contract/r/gnoswap/pool/v1/manager.gno
  • contract/r/gnoswap/pool/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/_mock_test.gno
  • contract/r/gnoswap/protocol_fee/proxy.gno
  • contract/r/gnoswap/protocol_fee/types.gno
  • contract/r/gnoswap/protocol_fee/v1/_mock_test.gno
  • contract/r/gnoswap/protocol_fee/v1/errors.gno
  • contract/r/gnoswap/protocol_fee/v1/getter.gno
  • contract/r/gnoswap/protocol_fee/v1/init.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee_state.gno
  • contract/r/gnoswap/protocol_fee/v1/protocol_fee_test.gno
  • contract/r/gnoswap/router/store.gno
  • contract/r/gnoswap/router/types.gno
  • contract/r/gnoswap/router/v1/_mock_test.gno
  • contract/r/gnoswap/router/v1/init.gno
  • contract/r/gnoswap/router/v1/protocol_fee_swap.gno
  • contract/r/gnoswap/staker/store.gno
  • contract/r/gnoswap/staker/types.gno
  • contract/r/gnoswap/staker/v1/_mock_test.gno
  • contract/r/gnoswap/staker/v1/init.gno
  • contract/r/gnoswap/staker/v1/protocol_fee_unstaking.gno
  • contract/r/gnoswap/staker/v1/protocol_fee_unstaking_test.gno
  • contract/r/gnoswap/test/fuzz/_mock_test.gno
  • contract/r/scenario/upgrade/implements/mock/protocol_fee/test_impl.gno
  • contract/r/scenario/upgrade/implements/v2_invalid/protocol_fee/test_impl.gno
  • contract/r/scenario/upgrade/implements/v3_valid/protocol_fee/test_impl.gno
  • tests/integration/testdata/gov/governance/create_parameter_change_proposal.txtar
  • tests/integration/testdata/gov/governance/execute_text_proposal_should_fail.txtar
💤 Files with no reviewable changes (1)
  • tests/integration/testdata/gov/governance/create_parameter_change_proposal.txtar
✅ Files skipped from review due to trivial changes (2)
  • contract/r/gnoswap/gov/staker/v1/state.gno
  • contract/r/gnoswap/protocol_fee/v1/errors.gno
🚧 Files skipped from review as they are similar to previous changes (18)
  • contract/r/gnoswap/pool/v1/init.gno
  • contract/r/gnoswap/protocol_fee/v1/init.gno
  • contract/r/gnoswap/staker/v1/protocol_fee_unstaking_test.gno
  • contract/r/gnoswap/router/types.gno
  • contract/r/gnoswap/pool/types.gno
  • contract/r/gnoswap/staker/store.gno
  • contract/r/gnoswap/staker/types.gno
  • contract/r/gnoswap/router/v1/_mock_test.gno
  • contract/r/gnoswap/pool/store.gno
  • contract/r/gnoswap/mock/pool_store.gno
  • contract/r/gnoswap/protocol_fee/v1/getter.gno
  • contract/r/gnoswap/mock/router_store.gno
  • contract/r/gnoswap/router/v1/init.gno
  • contract/r/gnoswap/staker/v1/_mock_test.gno
  • contract/r/scenario/upgrade/implements/v2_invalid/protocol_fee/test_impl.gno
  • contract/r/gnoswap/protocol_fee/proxy.gno
  • contract/r/gnoswap/protocol_fee/types.gno
  • contract/r/scenario/upgrade/implements/mock/protocol_fee/test_impl.gno

Comment on lines +109 to +112
poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
gns.TransferFrom(cross, previousRealmAddr, poolAddr, poolCreationFee)
i.addPendingProtocolFee(GNS_PATH, poolCreationFee)
i.tryCollectPendingProtocolFees()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the safe transfer wrapper and record pending fees before the token call.

gns.TransferFrom bypasses the project’s safe transfer wrapper, and the pending-fee state is written after the external token transfer. Use SafeGRC20TransferFrom and keep effects before interactions. As per coding guidelines, use SafeGRC20Transfer or SafeGRC20TransferFrom only for token transfers, and all state updates must occur before token transfers.

Proposed fix
-		gns.TransferFrom(cross, previousRealmAddr, poolAddr, poolCreationFee)
 		i.addPendingProtocolFee(GNS_PATH, poolCreationFee)
+		common.SafeGRC20TransferFrom(cross, GNS_PATH, previousRealmAddr, poolAddr, poolCreationFee)
 		i.tryCollectPendingProtocolFees()

If this removes the last gns usage, also drop the gno.land/r/gnoswap/gns import.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
gns.TransferFrom(cross, previousRealmAddr, poolAddr, poolCreationFee)
i.addPendingProtocolFee(GNS_PATH, poolCreationFee)
i.tryCollectPendingProtocolFees()
poolAddr := access.MustGetAddress(prabc.ROLE_POOL.String())
i.addPendingProtocolFee(GNS_PATH, poolCreationFee)
common.SafeGRC20TransferFrom(cross, GNS_PATH, previousRealmAddr, poolAddr, poolCreationFee)
i.tryCollectPendingProtocolFees()

Comment on lines +34 to +43
if err := pf.bookCollectedProtocolFees(); err != nil {
panic(err)
}

sentToDevOpsForEvent := make([]string, 0)
sentToGovStakerForEvent := make([]string, 0)
toReturnDistributedToGovStaker := make(map[string]int64)
if halt.IsHaltedWithdraw() {
return toReturnDistributedToGovStaker
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Emit an event when safe-mode booking mutates accounting.

bookCollectedProtocolFees() can move tokenListWithAmounts into accumulated state and clear the token list, but Line 42 returns before any event when withdraw is halted. Either move the halt check before booking if this path should be a true no-op, or emit a dedicated booking/deferred-settlement event so indexers can observe the state change. As per coding guidelines, all state changes MUST emit events for off-chain indexing.

Comment on lines 207 to 216
// Check for overflow
addedAmount := safeAddInt64(currentAmount, amount)
if amount == 0 {
pf.store.SetTokenListWithAmountItem(tokenPath, addedAmount)
return nil
}
protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
common.SafeGRC20TransferFrom(cross, tokenPath, caller, protocolFeeAddr, amount)

pf.store.SetTokenListWithAmountItem(tokenPath, addedAmount)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Persist accounting before transferring tokens.

Line 214 performs an external token transfer before Line 216 records the new protocol-fee amount. Move the store update before SafeGRC20TransferFrom; a transfer panic should still roll back the transaction, while reentrant/external execution cannot observe stale accounting. As per coding guidelines, all state updates must occur before token transfers (Checks-Effects-Interactions pattern).

🛡️ Proposed fix
 	// Check for overflow
 	addedAmount := safeAddInt64(currentAmount, amount)
+	pf.store.SetTokenListWithAmountItem(tokenPath, addedAmount)
 	if amount == 0 {
-		pf.store.SetTokenListWithAmountItem(tokenPath, addedAmount)
 		return nil
 	}
 	protocolFeeAddr := access.MustGetAddress(prabc.ROLE_PROTOCOL_FEE.String())
 	common.SafeGRC20TransferFrom(cross, tokenPath, caller, protocolFeeAddr, amount)
-
-	pf.store.SetTokenListWithAmountItem(tokenPath, addedAmount)
 
 	return nil

@junghoon-vans junghoon-vans force-pushed the GSW-2521-emergency-recovery-halt branch from 0775780 to 7c26d25 Compare April 21, 2026 02:22
@junghoon-vans junghoon-vans changed the base branch from main to GSW-2616-protocol-fee-pull April 21, 2026 02:22
@junghoon-vans junghoon-vans force-pushed the GSW-2616-protocol-fee-pull branch from 84dced3 to 48efe03 Compare April 22, 2026 02:56
@sonarqubecloud
Copy link
Copy Markdown

@junghoon-vans junghoon-vans added the DO NOT MERGE do not merge this PR label Apr 24, 2026
Base automatically changed from GSW-2616-protocol-fee-pull to main April 24, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

DO NOT MERGE do not merge this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants