protocol_fee_swap.gno

package router

import (
	"chain"
	"errors"

	ufmt "gno.land/p/nt/ufmt/v0"

	gnsmath "gno.land/p/gnoswap/gnsmath"
	prbac "gno.land/p/gnoswap/rbac"
	u256 "gno.land/p/gnoswap/uint256"
	"gno.land/p/gnoswap/utils"

	"gno.land/r/gnoswap/access"
	"gno.land/r/gnoswap/common"
	"gno.land/r/gnoswap/halt"

	pf "gno.land/r/gnoswap/protocol_fee"
	_ "gno.land/r/gnoswap/protocol_fee/v1"
)

// GetSwapFee returns the current swap fee rate in basis points.
func (r *routerV1) GetSwapFee() uint64 {
	return r.store.GetSwapFee()
}

// SetSwapFee sets the protocol swap fee rate.
//
// Fee is deducted from swap output and sent to protocol fee contract.
// Only callable by admin or governance.
//
// Parameters:
//   - fee: Fee rate in basis points (0-1000 = 0%-10%)
//
// Access:
//   - Requires: Admin or Governance role
//   - Halt check: Router and ProtocolFee must not be halted
//
// Events:
//   - SetSwapFee: Emits previous and new fee values
//
// Reverts if:
//   - Caller is not admin/governance
//   - Fee > 1000 bps (10%)
//   - Router or ProtocolFee is halted
func (r *routerV1) SetSwapFee(_ int, rlm realm, fee uint64) {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedRouter()
	halt.AssertIsNotHaltedProtocolFee()

	previousRealm := rlm.Previous()
	caller := previousRealm.Address()
	access.AssertIsAdminOrGovernance(caller)

	// max swap fee is 1000 (bps)
	if fee > 1000 {
		panic(ufmt.Errorf(
			"%s: fee must be in range 0 to 1000 (10%). got %d",
			errInvalidSwapFee, fee,
		))
	}

	prevSwapFee := r.store.GetSwapFee()
	if err := r.store.SetSwapFee(0, rlm, fee); err != nil {
		panic(err)
	}

	chain.Emit(
		"SetSwapFee",
		"prevAddr", previousRealm.Address().String(),
		"prevRealm", previousRealm.PkgPath(),
		"newFee", utils.FormatUint(fee),
		"prevFee", utils.FormatUint(prevSwapFee),
	)
}

// handleSwapFee deducts the protocol fee from the swap amount and transfers it to the protocol fee contract.
func (r *routerV1) handleSwapFee(
	_ int,
	rlm realm,
	outputToken string,
	amount int64,
) int64 {
	swapFee := r.store.GetSwapFee()
	if swapFee <= 0 {
		r.addToProtocolFee(0, rlm)
		return amount
	}

	currentTokenPath := outputToken

	feeAmountInt64 := calculateRouterFee(amount, swapFee)
	r.addPendingProtocolFee(0, rlm, currentTokenPath, feeAmountInt64)
	r.addToProtocolFee(0, rlm)

	previousRealm := rlm.Previous()
	chain.Emit(
		"SwapRouteFee",
		"prevAddr", previousRealm.Address().String(),
		"prevRealm", previousRealm.PkgPath(),
		"tokenPath", currentTokenPath,
		"amount", utils.FormatInt(feeAmountInt64),
	)

	return gnsmath.SafeSubInt64(amount, feeAmountInt64)
}

func (r *routerV1) addPendingProtocolFee(_ int, rlm realm, tokenPath string, amount int64) {
	if amount <= 0 {
		return
	}

	existingProtocolFees := r.store.GetPendingProtocolFees()
	pendingProtocolFees := make(map[string]int64)
	for pendingTokenPath, pendingAmount := range existingProtocolFees {
		pendingProtocolFees[pendingTokenPath] = pendingAmount
	}
	pendingProtocolFees[tokenPath] = gnsmath.SafeAddInt64(pendingProtocolFees[tokenPath], amount)
	if err := r.store.SetPendingProtocolFees(0, rlm, pendingProtocolFees); err != nil {
		panic(err)
	}
}

// addToProtocolFee sends pending router protocol fees to the protocol fee realm.
// It is best-effort: when protocol fee collection is halted, it keeps pending
// storage unchanged; otherwise it rebuilds pending storage with only entries
// that could not be added, removing successful and non-positive entries.
func (r *routerV1) addToProtocolFee(_ int, rlm realm) {
	pendingProtocolFees := r.store.GetPendingProtocolFees()
	if len(pendingProtocolFees) == 0 || halt.IsHaltedProtocolFee() {
		return
	}

	protocolFeeAddr := access.MustGetAddress(prbac.ROLE_PROTOCOL_FEE.String())
	remainingProtocolFees := make(map[string]int64)
	for tokenPath, amount := range pendingProtocolFees {
		if amount <= 0 {
			continue
		}

		common.SafeGRC20Approve(cross(rlm), tokenPath, protocolFeeAddr, amount)
		if err := pf.AddToProtocolFee(cross(rlm), tokenPath, amount); err != nil {
			remainingProtocolFees[tokenPath] = amount
			continue
		}
	}

	if err := r.store.SetPendingProtocolFees(0, rlm, remainingProtocolFees); err != nil {
		panic(err)
	}
}

func calculateRouterFee(amount int64, swapFee uint64) int64 {
	if swapFee <= 0 {
		return 0
	}

	feeAmount := u256.MulDiv(u256.NewUintFromInt64(amount), u256.NewUint(swapFee), u256.NewUint(10000))
	return gnsmath.SafeConvertToInt64(feeAmount)
}

// calculate amount to fetch from pool including router fee
// poolAmount = userAmount / (1 - feeRate)
// = userAmount * 10000 / (10000 - swapFeeBPS)
func calculateExactOutWithRouterFee(amount int64, swapFee uint64) int64 {
	if amount == 0 {
		return amount
	}

	if swapFee > 0 {
		// Use MulDiv to prevent overflow and maintain precision
		poolAmount := u256.MulDiv(
			u256.NewUintFromInt64(amount),
			u256.NewUint(10000),
			u256.NewUint(10000-swapFee),
		)

		return gnsmath.SafeConvertToInt64(poolAmount)
	}

	return amount
}