Skip to content

Commit

Permalink
[Application] Enforce minimum stake when burning (#848)
Browse files Browse the repository at this point in the history
## Summary

Updates `ProcessTokenLogicModules()` logic to unbond applications whose
stake is below the minimum **after** processing all TLMs.

## Dependencies

- #809
- #843 
- #844 
- #845
- #847

## Dependents
 
- #849
- #850
- #857
- #852 
- #861 
- #851 
- #863 

## Issue

- #612

## Type of change

Select one or more from the following:

- [x] New feature, functionality or library
- [ ] Consensus breaking; add the `consensus-breaking` label if so. See
#791 for details
- [ ] Bug fix
- [ ] Code health or cleanup
- [ ] Documentation
- [ ] Other (specify)

## Testing

- [ ] **Documentation**: `make docusaurus_start`; only needed if you
make doc changes
- [x] **Unit Tests**: `make go_develop_and_test`
- [ ] **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

---------

Co-authored-by: Redouane Lakrache <[email protected]>
Co-authored-by: Daniel Olshansky <[email protected]>
Co-authored-by: red-0ne <[email protected]>
  • Loading branch information
4 people authored Oct 11, 2024
1 parent 879401e commit b60dac9
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 35 deletions.
6 changes: 3 additions & 3 deletions config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ genesis:
max_delegated_gateways: "7"
min_stake:
# TODO_MAINNET: Determine realistic amount for minimum application stake amount.
amount: "1000000" # 1 POKT
amount: "100000000" # 100 POKT
denom: upokt
applicationList:
- address: pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4
Expand All @@ -179,7 +179,7 @@ genesis:
stake:
# NB: This value should be exactly 1upokt smaller than the value in
# `supplier1_stake_config.yaml` so that the stake command causes a state change.
amount: "1000068"
amount: "100000068" # ~100 POKT
denom: upokt
- address: pokt1ad28jdap2zfanjd7hpkh984yveney6k9a42man
delegatee_gateway_addresses: []
Expand All @@ -190,7 +190,7 @@ genesis:
stake:
# NB: This value should be exactly 1upokt smaller than the value in
# `supplier1_stake_config.yaml` so that the stake command causes a state change.
amount: "1000068"
amount: "100000068" # ~100 POKT
denom: upokt
supplier:
supplierList:
Expand Down
1 change: 1 addition & 0 deletions pkg/crypto/rings/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type ringClient struct {
// - polylog.Logger
// - client.ApplicationQueryClient
// - client.AccountQueryClient
// - client.SharedQueryClient
func NewRingClient(deps depinject.Config) (_ crypto.RingClient, err error) {
rc := new(ringClient)

Expand Down
188 changes: 188 additions & 0 deletions tests/integration/application/min_stake_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package application

import (
"context"
"testing"

cosmoslog "cosmossdk.io/log"
"cosmossdk.io/math"
cosmostypes "github.com/cosmos/cosmos-sdk/types"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/cmd/poktrolld/cmd"
_ "github.com/pokt-network/poktroll/pkg/polylog/polyzero"
"github.com/pokt-network/poktroll/testutil/keeper"
testproof "github.com/pokt-network/poktroll/testutil/proof"
"github.com/pokt-network/poktroll/testutil/sample"
apptypes "github.com/pokt-network/poktroll/x/application/types"
prooftypes "github.com/pokt-network/poktroll/x/proof/types"
sessiontypes "github.com/pokt-network/poktroll/x/session/types"
"github.com/pokt-network/poktroll/x/shared"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
)

type applicationMinStakeTestSuite struct {
suite.Suite

ctx context.Context
keepers keeper.TokenomicsModuleKeepers

serviceId,
appBech32,
supplierBech32 string

appStake *cosmostypes.Coin

numRelays,
numComputeUnitsPerRelay uint64
}

func TestApplicationMinStakeTestSuite(t *testing.T) {
cmd.InitSDKConfig()

suite.Run(t, new(applicationMinStakeTestSuite))
}

func (s *applicationMinStakeTestSuite) SetupTest() {
s.keepers, s.ctx = keeper.NewTokenomicsModuleKeepers(s.T(), cosmoslog.NewNopLogger())

s.serviceId = "svc1"
s.appBech32 = sample.AccAddress()
s.supplierBech32 = sample.AccAddress()
s.appStake = &apptypes.DefaultMinStake
s.numRelays = 10
s.numComputeUnitsPerRelay = 1

// Set block height to 1.
s.ctx = cosmostypes.UnwrapSDKContext(s.ctx).WithBlockHeight(1)
}

func (s *applicationMinStakeTestSuite) TestAppCannotStakeLessThanMinStake() {
s.T().Skip("this case is well covered in x/application/keeper/msg_server_stake_application_test.go")
}

func (s *applicationMinStakeTestSuite) TestAppIsUnbondedIfBelowMinStakeWhenSettling() {
// Assert that the application's initial bank balance is 0.
appBalance := s.getAppBalance()
require.Equal(s.T(), int64(0), appBalance.Amount.Int64())

// Add service 1
s.addService()

// Stake an application for service 1 with min stake.
s.stakeApp()

// Stake a supplier for service 1.
s.stakeSupplier()

// Get the session header.
sessionHeader := s.getSessionHeader()

// Create a claim whose settlement amount drops the application below min stake
claim := s.getClaim(sessionHeader)
s.keepers.ProofKeeper.UpsertClaim(s.ctx, *claim)

// Set the current height to the claim settlement height.
sdkCtx := cosmostypes.UnwrapSDKContext(s.ctx)
currentHeight := sdkCtx.BlockHeight()
sharedParams := s.keepers.SharedKeeper.GetParams(s.ctx)
currentSessionEndHeight := shared.GetSessionEndHeight(&sharedParams, currentHeight)
claimSettlementHeight := currentSessionEndHeight + int64(sharedtypes.GetSessionEndToProofWindowCloseBlocks(&sharedParams)) + 1
sdkCtx = sdkCtx.WithBlockHeight(claimSettlementHeight)
s.ctx = sdkCtx

// Settle pending claims; this should cause the application to be unbonded.
_, _, err := s.keepers.Keeper.SettlePendingClaims(sdkCtx)
require.NoError(s.T(), err)

// Assert that the application was unbonded.
_, isAppFound := s.keepers.ApplicationKeeper.GetApplication(s.ctx, s.appBech32)
require.False(s.T(), isAppFound)

// Assert that the application's stake was returned to its bank balance.
expectedAppBurn := math.NewInt(int64(s.numRelays * s.numComputeUnitsPerRelay * sharedtypes.DefaultComputeUnitsToTokensMultiplier))
expectedAppBalance := s.appStake.SubAmount(expectedAppBurn)
appBalance = s.getAppBalance()
require.Equal(s.T(), expectedAppBalance.Amount.Int64(), appBalance.Amount.Int64())

}

// addService adds the test service to the service module state.
func (s *applicationMinStakeTestSuite) addService() {
s.keepers.ServiceKeeper.SetService(s.ctx, sharedtypes.Service{
Id: s.serviceId,
ComputeUnitsPerRelay: 1,
OwnerAddress: sample.AccAddress(), // random address.
})
}

// stakeApp stakes an application for service 1 with min stake.
func (s *applicationMinStakeTestSuite) stakeApp() {
s.keepers.ApplicationKeeper.SetApplication(s.ctx, apptypes.Application{
Address: s.appBech32,
Stake: s.appStake,
ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{{ServiceId: s.serviceId}},
})
}

// stakeSupplier stakes a supplier for service 1.
func (s *applicationMinStakeTestSuite) stakeSupplier() {
// TODO_UPNEXT(@bryanchriswhite, #612): Replace supplierStake with suppleirtypes.DefaultMinStake.
supplierStake := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 1000000) // 1 POKT.
s.keepers.SupplierKeeper.SetSupplier(s.ctx, sharedtypes.Supplier{
OwnerAddress: s.supplierBech32,
OperatorAddress: s.supplierBech32,
Stake: &supplierStake,
Services: []*sharedtypes.SupplierServiceConfig{
{
ServiceId: s.serviceId,
RevShare: []*sharedtypes.ServiceRevenueShare{
{
Address: s.supplierBech32,
RevSharePercentage: 100,
},
},
},
},
})
}

// getSessionHeader gets the session header for the test session.
func (s *applicationMinStakeTestSuite) getSessionHeader() *sessiontypes.SessionHeader {
sdkCtx := cosmostypes.UnwrapSDKContext(s.ctx)
currentHeight := sdkCtx.BlockHeight()
sessionRes, err := s.keepers.SessionKeeper.GetSession(s.ctx, &sessiontypes.QueryGetSessionRequest{
ApplicationAddress: s.appBech32,
ServiceId: s.serviceId,
BlockHeight: currentHeight,
})
require.NoError(s.T(), err)

return sessionRes.GetSession().GetHeader()
}

// getClaim creates a claim whose settlement amount drops the application below min stake.
func (s *applicationMinStakeTestSuite) getClaim(
sessionHeader *sessiontypes.SessionHeader,
) *prooftypes.Claim {
claimRoot := testproof.SmstRootWithSumAndCount(s.numRelays*s.numComputeUnitsPerRelay, s.numRelays)

return &prooftypes.Claim{
SupplierOperatorAddress: s.supplierBech32,
SessionHeader: sessionHeader,
RootHash: claimRoot,
}
}

// getAppBalance returns the bank module balance for the application.
func (s *applicationMinStakeTestSuite) getAppBalance() *cosmostypes.Coin {
appBalRes, err := s.keepers.BankKeeper.Balance(s.ctx, &banktypes.QueryBalanceRequest{
Address: s.appBech32, Denom: volatile.DenomuPOKT,
})
require.NoError(s.T(), err)

return appBalRes.GetBalance()
}
4 changes: 4 additions & 0 deletions testutil/keeper/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
mockApplicationKeeper.EXPECT().
SetApplication(gomock.Any(), gomock.Any()).
AnyTimes()
mockApplicationKeeper.EXPECT().
UnbondApplication(gomock.Any(), gomock.Any()).
Return(nil).
AnyTimes()

// Mock the supplier keeper.
mockSupplierKeeper := mocks.NewMockSupplierKeeper(ctrl)
Expand Down
4 changes: 3 additions & 1 deletion testutil/testkeyring/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,12 @@ func (iter *PreGeneratedAccountIterator) Next() (_ *PreGeneratedAccount, ok bool
return iter.accounts[currentIndex], true
}

// MustNext returns the next account in the iterator. It panics if the iterator
// is out of accounts; see testutil/testkeyring/gen_accounts.
func (iter *PreGeneratedAccountIterator) MustNext() *PreGeneratedAccount {
account, ok := iter.Next()
if !ok {
panic("insufficient number of pre-generated accounts")
panic("insufficient number of pre-generated accounts; see testutil/testkeyring/gen_accounts")
}
return account
}
Expand Down
9 changes: 6 additions & 3 deletions x/application/keeper/msg_server_stake_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func (k msgServer) StakeApplication(ctx context.Context, msg *types.MsgStakeAppl
logger.Info(fmt.Sprintf("Application is going to escrow an additional %+v coins", coinsToEscrow))

// If the application has initiated an unstake action, cancel it since it is staking again.
// TODO_UPNEXT:(@bryanchriswhite): assert that an EventApplicationUnbondingCanceled event was emitted.
foundApp.UnstakeSessionEndHeight = types.ApplicationNotUnstaking
}

Expand All @@ -67,12 +68,14 @@ func (k msgServer) StakeApplication(ctx context.Context, msg *types.MsgStakeAppl

// MUST ALWAYS have at least minimum stake.
minStake := k.GetParams(ctx).MinStake
// TODO_CONSIDERATION: If we support multiple native tokens, we will need to
// start checking the denom here.
if msg.Stake.Amount.LT(minStake.Amount) {
errFmt := "application %q must stake at least %s"
logger.Info(fmt.Sprintf(errFmt, msg.Address, minStake))
err = fmt.Errorf("application %q must stake at least %s", msg.GetAddress(), minStake)
logger.Info(err.Error())
return nil, status.Error(
codes.InvalidArgument,
types.ErrAppInvalidStake.Wrapf(errFmt, msg.Address, minStake).Error(),
types.ErrAppInvalidStake.Wrapf("%s", err).Error(),
)
}

Expand Down
2 changes: 2 additions & 0 deletions x/application/keeper/msg_server_unstake_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func (k msgServer) UnstakeApplication(
foundApp.UnstakeSessionEndHeight = uint64(shared.GetSessionEndHeight(&sharedParams, currentHeight))
k.SetApplication(ctx, foundApp)

// TODO_UPNEXT:(@bryanchriswhite): emit a new EventApplicationUnbondingBegin event.

isSuccessful = true
return &types.MsgUnstakeApplicationResponse{}, nil
}
8 changes: 8 additions & 0 deletions x/application/keeper/msg_server_unstake_application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) {
_, err = srv.UnstakeApplication(ctx, unstakeMsg)
require.NoError(t, err)

// TODO_UPNEXT:(@bryanchriswhite): assert that an EventApplicationUnbondingBegin event was emitted.

// Make sure the application entered the unbonding period
foundApp, isAppFound = applicationModuleKeepers.GetApplication(ctx, unstakingAppAddr)
require.True(t, isAppFound)
Expand All @@ -70,6 +72,8 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) {
err = applicationModuleKeepers.EndBlockerUnbondApplications(ctx)
require.NoError(t, err)

// TODO_UPNEXT:(@bryanchriswhite): assert that an EventApplicationUnbondingEnd event was emitted.

// Make sure the unstaking application is removed from the applications list when
// the unbonding period is over.
_, isAppFound = applicationModuleKeepers.GetApplication(ctx, unstakingAppAddr)
Expand Down Expand Up @@ -105,6 +109,8 @@ func TestMsgServer_UnstakeApplication_CancelUnbondingIfRestaked(t *testing.T) {
_, err = srv.UnstakeApplication(ctx, unstakeMsg)
require.NoError(t, err)

// TODO_UPNEXT:(@bryanchriswhite): assert that an EventApplicationUnbondingBegin event was emitted.

// Make sure the application entered the unbonding period
foundApp, isAppFound = applicationModuleKeepers.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
Expand All @@ -117,6 +123,8 @@ func TestMsgServer_UnstakeApplication_CancelUnbondingIfRestaked(t *testing.T) {
_, err = srv.StakeApplication(ctx, stakeMsg)
require.NoError(t, err)

// TODO_UPNEXT:(@bryanchriswhite): assert that an EventApplicationUnbondingCanceled event was emitted.

// Make sure the application is no longer in the unbonding period
foundApp, isAppFound = applicationModuleKeepers.GetApplication(ctx, appAddr)
require.True(t, isAppFound)
Expand Down
Loading

0 comments on commit b60dac9

Please sign in to comment.