Skip to content

Commit

Permalink
[Supplier] Add supplier staking fee (#883)
Browse files Browse the repository at this point in the history
## Summary

Replace the upstaking requirement with a staking fee when a stake is
added or updated.

The staking fee will become a governance parameter in a follow up PR.

## Issue

- #853 

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library

## Testing

- [x] **Unit Tests**: `make go_develop_and_test`
- [x] **LocalNet E2E Tests**: `make test_e2e`
- [ ] **DevNet E2E Tests**: Add the `devnet-test-e2e` label to the PR.

## Sanity Checklist

- [x] I have tested my changes using the available tooling
- [x] I have commented my code
- [x] I have performed a self-review of my own code; both comments &
source code
- [ ] I create and reference any new tickets, if applicable
- [x] I have left TODOs throughout the codebase, if applicable
  • Loading branch information
red-0ne authored Oct 28, 2024
1 parent eb74628 commit 1059c24
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 24 deletions.
5 changes: 3 additions & 2 deletions e2e/tests/stake_supplier.feature
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ Feature: Stake Supplier Namespace

Scenario: User can stake a Supplier
Given the user has the pocketd binary installed
# TODO_UPNEXT: Set the supplier stake fee once it is a param.
And the user verifies the "supplier" for account "supplier2" is not staked
And the account "supplier2" has a balance greater than "1000070" uPOKT
And the account "supplier2" has a balance greater than "1000071" uPOKT
When the user stakes a "supplier" with "1000070" uPOKT for "anvil" service from the account "supplier2"
Then the user should be able to see standard output containing "txhash:"
And the user should be able to see standard output containing "code: 0"
And the pocketd binary should exit without error
And the user should wait for the "supplier" module "StakeSupplier" message to be submitted
And the user should wait for the "supplier" module "SupplierStaked" tx event to be broadcast
And the "supplier" for account "supplier2" is staked with "1000070" uPOKT
And the account balance of "supplier2" should be "1000070" uPOKT "less" than before
And the account balance of "supplier2" should be "1000071" uPOKT "less" than before

Scenario: User can unstake a Supplier
Given the user has the pocketd binary installed
Expand Down
20 changes: 11 additions & 9 deletions testutil/keeper/supplier.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ type SupplierModuleKeepers struct {
*keeper.Keeper
types.SharedKeeper
// Tracks the amount of funds returned to the supplier owner when the supplier is unbonded.
SupplierUnstakedFundsMap map[string]int64
SupplierBalanceMap map[string]int64
}

func SupplierKeeper(t testing.TB) (SupplierModuleKeepers, context.Context) {
Expand All @@ -53,17 +53,19 @@ func SupplierKeeper(t testing.TB) (SupplierModuleKeepers, context.Context) {
cdc := codec.NewProtoCodec(registry)
authority := authtypes.NewModuleAddress(govtypes.ModuleName)

// Set a simple map to track the where the supplier stake is returned when
// the supplier is unbonded.
supplierUnstakedFundsMap := make(map[string]int64)
// Set a simple map to track the suppliers balances.
supplierBalanceMap := make(map[string]int64)

ctrl := gomock.NewController(t)
mockBankKeeper := mocks.NewMockBankKeeper(ctrl)
mockBankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes()
mockBankKeeper.EXPECT().SendCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes().
Do(func(ctx context.Context, addr sdk.AccAddress, module string, coins sdk.Coins) {
supplierBalanceMap[addr.String()] -= coins[0].Amount.Int64()
})
mockBankKeeper.EXPECT().SpendableCoins(gomock.Any(), gomock.Any()).AnyTimes()
mockBankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).AnyTimes().
Do(func(ctx context.Context, module string, addr sdk.AccAddress, coins sdk.Coins) {
supplierUnstakedFundsMap[addr.String()] += coins[0].Amount.Int64()
supplierBalanceMap[addr.String()] += coins[0].Amount.Int64()
})

// Construct a real shared keeper.
Expand Down Expand Up @@ -104,9 +106,9 @@ func SupplierKeeper(t testing.TB) (SupplierModuleKeepers, context.Context) {
serviceKeeper.SetService(ctx, sharedtypes.Service{Id: "svcId2"})

supplierModuleKeepers := SupplierModuleKeepers{
Keeper: &supplierKeeper,
SharedKeeper: sharedKeeper,
SupplierUnstakedFundsMap: supplierUnstakedFundsMap,
Keeper: &supplierKeeper,
SharedKeeper: sharedKeeper,
SupplierBalanceMap: supplierBalanceMap,
}

return supplierModuleKeepers, ctx
Expand Down
29 changes: 23 additions & 6 deletions x/supplier/keeper/msg_server_stake_supplier.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,19 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/telemetry"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
"github.com/pokt-network/poktroll/x/supplier/types"
)

var (
// TODO_BETA: Make supplier staking fee a governance parameter
// TODO_BETA(@red-0ne): Update supplier staking documentation to remove the upstaking
// requirement and introduce the staking fee.
SupplierStakingFee = sdk.NewInt64Coin(volatile.DenomuPOKT, 1)
)

func (k msgServer) StakeSupplier(ctx context.Context, msg *types.MsgStakeSupplier) (*types.MsgStakeSupplierResponse, error) {
isSuccessful := false
defer telemetry.EventSuccessCounter(
Expand Down Expand Up @@ -104,9 +112,13 @@ func (k msgServer) StakeSupplier(ctx context.Context, msg *types.MsgStakeSupplie
supplier.UnstakeSessionEndHeight = sharedtypes.SupplierNotUnstaking
}

// MUST ALWAYS stake or upstake (> 0 delta)
if coinsToEscrow.IsZero() {
err = types.ErrSupplierInvalidStake.Wrapf("Signer %q must escrow more than 0 additional coins", msg.Signer)
// TODO_BETA: Remove requirement of MUST ALWAYS stake or upstake (>= 0 delta)
// TODO_POST_MAINNET: Should we allow stake decrease down to min stake?
if coinsToEscrow.IsNegative() {
err = types.ErrSupplierInvalidStake.Wrapf(
"Supplier signer %q stake (%s) must be greater than or equal to the current stake (%s)",
msg.Signer, msg.GetStake(), supplier.Stake,
)
logger.Info(fmt.Sprintf("WARN: %s", err))
return nil, status.Error(codes.InvalidArgument, err.Error())
}
Expand All @@ -130,7 +142,8 @@ func (k msgServer) StakeSupplier(ctx context.Context, msg *types.MsgStakeSupplie
}

// Send the coins from the message signer account to the staked supplier pool
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, msgSignerAddress, types.ModuleName, []sdk.Coin{coinsToEscrow})
stakeWithFee := sdk.NewCoins(coinsToEscrow.Add(SupplierStakingFee))
err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, msgSignerAddress, types.ModuleName, stakeWithFee)
if err != nil {
logger.Info(fmt.Sprintf("ERROR: could not send %v coins from %q to %q module account due to %v", coinsToEscrow, msgSignerAddress, types.ModuleName, err))
return nil, status.Error(codes.InvalidArgument, err.Error())
Expand Down Expand Up @@ -191,8 +204,12 @@ func (k msgServer) updateSupplier(
return types.ErrSupplierInvalidStake.Wrapf("stake amount cannot be nil")
}

if msg.Stake.IsLTE(*supplier.Stake) {
return types.ErrSupplierInvalidStake.Wrapf("stake amount %v must be higher than previous stake amount %v", msg.Stake, supplier.Stake)
// TODO_BETA: No longer require upstaking. Remove this check.
if msg.Stake.IsLT(*supplier.Stake) {
return types.ErrSupplierInvalidStake.Wrapf(
"stake amount %v must be greater than or equal than previous stake amount %v",
msg.Stake, supplier.Stake,
)
}
supplier.Stake = msg.Stake

Expand Down
15 changes: 11 additions & 4 deletions x/supplier/keeper/msg_server_stake_supplier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,16 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {
require.Equal(t, "svcId", foundSupplier.Services[0].ServiceId)
require.Len(t, foundSupplier.Services[0].Endpoints, 1)
require.Equal(t, "http://localhost:8080", foundSupplier.Services[0].Endpoints[0].Url)

// Prepare an updated supplier with a higher stake and a different URL for the service
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 2000000, "svcId2")
// Assert that the supplier's account balance was reduced by the staking fee
balanceDecrease := keeper.SupplierStakingFee.Amount.Int64() + foundSupplier.Stake.Amount.Int64()
// SupplierBalanceMap reflects the relative changes to the supplier's balance
// (i.e. it starts from 0 and can go below it).
// It is not using coins that enforce non-negativity of the balance nor account
// funding and lookups.
require.Equal(t, -balanceDecrease, supplierModuleKeepers.SupplierBalanceMap[ownerAddr])

// Prepare an updated supplier with the same stake and a different URL for the service
updateMsg := stakeSupplierForServicesMsg(ownerAddr, operatorAddr, 1000000, "svcId2")
updateMsg.Services[0].Endpoints[0].Url = "http://localhost:8082"

// Update the staked supplier
Expand All @@ -56,7 +63,7 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) {

foundSupplier, isSupplierFound = supplierModuleKeepers.GetSupplier(ctx, operatorAddr)
require.True(t, isSupplierFound)
require.Equal(t, int64(2000000), foundSupplier.Stake.Amount.Int64())
require.Equal(t, int64(1000000), foundSupplier.Stake.Amount.Int64())
require.Len(t, foundSupplier.Services, 1)
require.Equal(t, "svcId2", foundSupplier.Services[0].ServiceId)
require.Len(t, foundSupplier.Services[0].Endpoints, 1)
Expand Down
10 changes: 7 additions & 3 deletions x/supplier/keeper/msg_server_unstake_supplier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,15 +226,19 @@ func TestMsgServer_UnstakeSupplier_OperatorCanUnstake(t *testing.T) {
unbondingHeight := sharedtypes.GetSupplierUnbondingHeight(&sharedParams, &foundSupplier)
ctx = keepertest.SetBlockHeight(ctx, int64(unbondingHeight))

// Balance decrease is the total amount deducted from the supplier's balance, including
// the initial stake and the staking fee.
balanceDecrease := keeper.SupplierStakingFee.Amount.Int64() + foundSupplier.Stake.Amount.Int64()
// Ensure that the initial stake is not returned to the owner yet
require.Equal(t, int64(0), supplierModuleKeepers.SupplierUnstakedFundsMap[ownerAddr])
require.Equal(t, -balanceDecrease, supplierModuleKeepers.SupplierBalanceMap[ownerAddr])

// Run the endblocker to unbond suppliers
err = supplierModuleKeepers.EndBlockerUnbondSuppliers(ctx)
require.NoError(t, err)

// Ensure that the initial stake is returned to the owner
require.Equal(t, initialStake, supplierModuleKeepers.SupplierUnstakedFundsMap[ownerAddr])
// Ensure that the initial stake is returned to the owner while the staking fee
// remains deducted from the supplier's balance.
require.Equal(t, -keeper.SupplierStakingFee.Amount.Int64(), supplierModuleKeepers.SupplierBalanceMap[ownerAddr])
}

func createStakeMsg(supplierOwnerAddr string, stakeAmount int64) *suppliertypes.MsgStakeSupplier {
Expand Down

0 comments on commit 1059c24

Please sign in to comment.