Skip to content

Commit

Permalink
[Tokenomics] Implement Global Mint Reimbursement Request (#878)
Browse files Browse the repository at this point in the history
## Summary

This PR adds
* GMRR TLM
* Ensures that the application has enough funds to cover for it.
* Deducts the total global mint amount from the application's stake.
* Sends the global mint amount from the application module account to
the tokenomics module account to the PNF account.
* The application still gets its global inflation share sent to its
account.

## Issue

- #732 

## 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

---------

Co-authored-by: Daniel Olshansky <[email protected]>
  • Loading branch information
red-0ne and Olshansk authored Oct 30, 2024
1 parent 4d1a423 commit ff76430
Show file tree
Hide file tree
Showing 11 changed files with 1,765 additions and 183 deletions.
953 changes: 909 additions & 44 deletions api/poktroll/tokenomics/event.pulsar.go

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions e2e/tests/0_settlement.feature
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# that can be used to clear the state of the chain between tests.

Feature: Tokenomics Namespace
Scenario: Settle the session when a valid claim is within max limits and a valid proof is submitted and required via threshold
Scenario: TLM Mint=Burn when a valid claim is within max limits and a valid proof is submitted and required via threshold
# Baseline
Given the user has the pocketd binary installed
# Network preparation and validation
Expand Down Expand Up @@ -35,12 +35,13 @@ Feature: Tokenomics Namespace
And the user should wait for the ClaimSettled event with "THRESHOLD" proof requirement to be broadcast
# Validate the results
# Please note that supplier mint is > app burn because of inflation
# TODO_TECHDEBT: Update this test such the the inflation is set and enforce that Mint=Burn
# TODO_TECHDEBT: Update this test such the inflation is set and enforce that Mint=Burn
# Then add a separate test that only validates that inflation is enforced correctly
Then the account balance of "supplier1" should be "898" uPOKT "more" than before
And the "application" stake of "app1" should be "840" uPOKT "less" than before
# The application stake should be less 840 * (1 + glbal_inflation) = 840 * 1.1 = 924
And the "application" stake of "app1" should be "924" uPOKT "less" than before

Scenario: Settle the session when a valid claim is create but not required
Scenario: TLM Mint=Burn when a valid claim is create but not required
# Baseline
Given the user has the pocketd binary installed
# Network preparation and validation
Expand Down Expand Up @@ -70,9 +71,10 @@ Feature: Tokenomics Namespace
And the user should wait for the ClaimSettled event with "NOT_REQUIRED" proof requirement to be broadcast
# Validate the results
# Please note that supplier mint is > app burn because of inflation
# TODO_TECHDEBT: Update this test such the the inflation is set and enforce that Mint=Burn
# TODO_TECHDEBT: Update this test such the inflation is set and enforce that Mint=Burn
Then the account balance of "supplier1" should be "449" uPOKT "more" than before
And the "application" stake of "app1" should be "420" uPOKT "less" than before
# The application stake should be less 420 * (1 + glbal_inflation) = 420 * 1.1 = 462
And the "application" stake of "app1" should be "462" uPOKT "less" than before

# TODO_TEST: Implement the following scenarios
# Scenario: Supplier revenue shares are properly distributed
Expand Down
11 changes: 11 additions & 0 deletions proto/poktroll/tokenomics/event.proto
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,15 @@ message EventSupplierSlashed {
// Amount slashed from the supplier's stake due to the expired claims.
// This is a function of the number of expired claims and proof missing penalty.
cosmos.base.v1beta1.Coin slashing_amount = 3;
}

// EventApplicationReimbursementRequest is emitted when an application requests
// a reimbursement.
message EventApplicationReimbursementRequest {
string application_addr = 1;
string supplier_operator_addr = 2;
string supplier_owner_addr = 3;
string service_id = 4;
string session_id = 5;
cosmos.base.v1beta1.Coin amount = 6;
}
14 changes: 9 additions & 5 deletions tests/integration/application/min_stake_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"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"
Expand All @@ -25,6 +24,7 @@ import (
sessiontypes "github.com/pokt-network/poktroll/x/session/types"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
suppliertypes "github.com/pokt-network/poktroll/x/supplier/types"
tokenomicskeeper "github.com/pokt-network/poktroll/x/tokenomics/keeper"
)

type applicationMinStakeTestSuite struct {
Expand All @@ -51,7 +51,7 @@ func TestApplicationMinStakeTestSuite(t *testing.T) {
}

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

proofParams := prooftypes.DefaultParams()
proofParams.ProofRequestProbability = 0
Expand Down Expand Up @@ -235,7 +235,8 @@ func (s *applicationMinStakeTestSuite) getExpectedApp(claim *prooftypes.Claim) *
expectedBurnCoin, err := claim.GetClaimeduPOKT(sharedParams, relayMiningDifficulty)
require.NoError(s.T(), err)

expectedEndStake := s.appStake.Sub(expectedBurnCoin)
globalInflationAmt, _ := tokenomicskeeper.CalculateGlobalPerClaimMintInflationFromSettlementAmount(expectedBurnCoin)
expectedEndStake := s.appStake.Sub(expectedBurnCoin).Sub(globalInflationAmt)
return &apptypes.Application{
Address: s.appBech32,
Stake: &expectedEndStake,
Expand Down Expand Up @@ -304,8 +305,11 @@ func (s *applicationMinStakeTestSuite) assertUnbondingEndEventObserved(expectedA
func (s *applicationMinStakeTestSuite) assertAppStakeIsReturnedToBalance() {
s.T().Helper()

expectedAppBurn := math.NewInt(int64(s.numRelays * s.numComputeUnitsPerRelay * sharedtypes.DefaultComputeUnitsToTokensMultiplier))
expectedAppBalance := s.appStake.SubAmount(expectedAppBurn)
expectedAppBurn := int64(s.numRelays * s.numComputeUnitsPerRelay * sharedtypes.DefaultComputeUnitsToTokensMultiplier)
expectedAppBurnCoin := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, expectedAppBurn)
globalInflationCoin, _ := tokenomicskeeper.CalculateGlobalPerClaimMintInflationFromSettlementAmount(expectedAppBurnCoin)
expectedAppBalance := s.appStake.Sub(expectedAppBurnCoin).Sub(globalInflationCoin)

appBalance := s.getAppBalance()
require.Equal(s.T(), expectedAppBalance.Amount.Int64(), appBalance.Amount.Int64())
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package integration_test

import (
"math"
"math/big"
"testing"

Expand Down Expand Up @@ -84,6 +83,7 @@ func TestComputeNewDifficultyHash_RewardsReflectWorkCompleted(t *testing.T) {
testutils.WithService(service),
testutils.WithApplication(application),
testutils.WithSupplier(supplier),
testutils.WithProofRequirement(false),
)
sdkCtx := sdk.UnwrapSDKContext(ctx)
sdkCtx = sdkCtx.WithBlockHeight(1)
Expand All @@ -94,13 +94,6 @@ func TestComputeNewDifficultyHash_RewardsReflectWorkCompleted(t *testing.T) {
err := keepers.SharedKeeper.SetParams(sdkCtx, sharedParams)
require.NoError(t, err)

// Set the global proof params so we never need a proof (for simplicity of this test)
err = keepers.ProofKeeper.SetParams(sdkCtx, prooftypes.Params{
ProofRequestProbability: 0, // we never need a proof randomly
ProofRequirementThreshold: &sdk.Coin{Denom: volatile.DenomuPOKT, Amount: sdkmath.NewInt(math.MaxInt64)}, // a VERY high threshold
})
require.NoError(t, err)

// Update the relay mining difficulty so there's always a difficulty to retrieve
// for the test service.
_, err = keepers.ServiceKeeper.UpdateRelayMiningDifficulty(sdkCtx, map[string]uint64{service.Id: 1})
Expand Down
45 changes: 40 additions & 5 deletions testutil/keeper/tokenomics.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package keeper

import (
"context"
"math"
"testing"

"cosmossdk.io/log"
"cosmossdk.io/math"
cosmosmath "cosmossdk.io/math"
"cosmossdk.io/store"
"cosmossdk.io/store/metrics"
storetypes "cosmossdk.io/store/types"
Expand All @@ -29,6 +30,7 @@ import (
"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/app"
"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/testutil/sample"
"github.com/pokt-network/poktroll/testutil/tokenomics/mocks"
appkeeper "github.com/pokt-network/poktroll/x/application/keeper"
Expand Down Expand Up @@ -117,7 +119,7 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
// Prepare the test application.
application := apptypes.Application{
Address: sample.AccAddress(),
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100000)},
Stake: &sdk.Coin{Denom: "upokt", Amount: cosmosmath.NewInt(100000)},
ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{{ServiceId: service.Id}},
}

Expand All @@ -126,7 +128,7 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
supplier := sharedtypes.Supplier{
OwnerAddress: supplierOwnerAddr,
OperatorAddress: supplierOwnerAddr,
Stake: &sdk.Coin{Denom: "upokt", Amount: math.NewInt(100000)},
Stake: &sdk.Coin{Denom: "upokt", Amount: cosmosmath.NewInt(100000)},
Services: []*sharedtypes.SupplierServiceConfig{
{
ServiceId: service.Id,
Expand Down Expand Up @@ -199,6 +201,9 @@ func TokenomicsKeeperWithActorAddrs(t testing.TB) (
mockBankKeeper.EXPECT().
SendCoinsFromModuleToModule(gomock.Any(), tokenomicstypes.ModuleName, suppliertypes.ModuleName, gomock.Any()).
AnyTimes()
mockBankKeeper.EXPECT().
SendCoinsFromModuleToModule(gomock.Any(), apptypes.ModuleName, tokenomicstypes.ModuleName, gomock.Any()).
AnyTimes()

// Mock the account keeper
mockAccountKeeper := mocks.NewMockAccountKeeper(ctrl)
Expand Down Expand Up @@ -346,9 +351,9 @@ func NewTokenomicsModuleKeepers(
require.NoError(t, bankKeeper.SetParams(sdkCtx, banktypes.DefaultParams()))

// Provide some initial funds to the suppliers & applications module accounts.
err = bankKeeper.MintCoins(sdkCtx, suppliertypes.ModuleName, sdk.NewCoins(sdk.NewCoin("upokt", math.NewInt(1000000000000))))
err = bankKeeper.MintCoins(sdkCtx, suppliertypes.ModuleName, sdk.NewCoins(sdk.NewCoin("upokt", cosmosmath.NewInt(1000000000000))))
require.NoError(t, err)
err = bankKeeper.MintCoins(sdkCtx, apptypes.ModuleName, sdk.NewCoins(sdk.NewCoin("upokt", math.NewInt(1000000000000))))
err = bankKeeper.MintCoins(sdkCtx, apptypes.ModuleName, sdk.NewCoins(sdk.NewCoin("upokt", cosmosmath.NewInt(1000000000000))))
require.NoError(t, err)

// Construct a real shared keeper.
Expand Down Expand Up @@ -513,3 +518,33 @@ func WithProposerAddr(addr string) TokenomicsModuleKeepersOpt {
return sdkCtx
}
}

// WithProofRequirement is an option to enable or disable the proof requirement
// in the tokenomics module keepers by setting the proof request probability to
// 1 or 0, respectively whie setting the proof requirement threshold to 0 or
// MaxInt64, respectively.
func WithProofRequirement(proofRequired bool) TokenomicsModuleKeepersOpt {
return func(ctx context.Context, keepers *TokenomicsModuleKeepers) context.Context {

proofParams := keepers.ProofKeeper.GetParams(ctx)
if proofRequired {
// Require a proof 100% of the time probabilistically speaking.
proofParams.ProofRequestProbability = 1
// Require a proof of any claim amount (i.e. anything greater than 0).
proofRequirementThreshold := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, 0)
proofParams.ProofRequirementThreshold = &proofRequirementThreshold
} else {
// Never require a proof probabilistically speaking.
proofParams.ProofRequestProbability = 0
// Require a proof for MaxInt64 claim amount (i.e. should never trigger).
proofRequirementThreshold := cosmostypes.NewInt64Coin(volatile.DenomuPOKT, math.MaxInt64)
proofParams.ProofRequirementThreshold = &proofRequirementThreshold
}

if err := keepers.ProofKeeper.SetParams(ctx, proofParams); err != nil {
panic(err)
}

return ctx
}
}
47 changes: 46 additions & 1 deletion x/tokenomics/keeper/settle_pending_claims.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package keeper

import (
"context"
"fmt"

"cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"

"github.com/pokt-network/poktroll/app/volatile"
apptypes "github.com/pokt-network/poktroll/x/application/types"
prooftypes "github.com/pokt-network/poktroll/x/proof/types"
servicekeeper "github.com/pokt-network/poktroll/x/service/keeper"
sharedtypes "github.com/pokt-network/poktroll/x/shared/types"
Expand Down Expand Up @@ -35,6 +37,15 @@ func (k Keeper) SettlePendingClaims(ctx sdk.Context) (
return settledResult, expiredResult, err
}

// Capture the applications initial stake which will be used to calculate the
// max share any claim could burn from the application stake.
// This ensures that each supplier can calculate the maximum amount it can take
// from an application's stake.
applicationInitialStakeMap, err := k.getApplicationInitialStakeMap(ctx, expiringClaims)
if err != nil {
return settledResult, expiredResult, err
}

blockHeight := ctx.BlockHeight()

logger.Info(fmt.Sprintf("found %d expiring claims at block height %d", len(expiringClaims), blockHeight))
Expand Down Expand Up @@ -176,8 +187,11 @@ func (k Keeper) SettlePendingClaims(ctx sdk.Context) (
// 1. The claim does not require a proof.
// 2. The claim requires a proof and a valid proof was found.

appAddress := claim.GetSessionHeader().GetApplicationAddress()
applicationInitialStake := applicationInitialStakeMap[appAddress]

// Manage the mint & burn accounting for the claim.
if err = k.ProcessTokenLogicModules(ctx, &claim); err != nil {
if err = k.ProcessTokenLogicModules(ctx, &claim, applicationInitialStake); err != nil {
logger.Error(fmt.Sprintf("error processing token logic modules for claim %q: %v", claim.SessionHeader.SessionId, err))
return settledResult, expiredResult, err
}
Expand Down Expand Up @@ -402,3 +416,34 @@ func (k Keeper) slashSupplierStake(

return nil
}

// getApplicationInitialStakeMap returns a map from an application address to the
// initial stake of the application. This is used to calculate the maximum share
// any claim could burn from the application stake.
func (k Keeper) getApplicationInitialStakeMap(
ctx context.Context,
expiringClaims []prooftypes.Claim,
) (applicationInitialStakeMap map[string]sdk.Coin, err error) {
applicationInitialStakeMap = make(map[string]sdk.Coin)
for _, claim := range expiringClaims {
appAddress := claim.SessionHeader.ApplicationAddress
// The same application is participating in other claims being settled,
// so we already capture its initial stake.
if _, isAppFound := applicationInitialStakeMap[appAddress]; isAppFound {
continue
}

app, isAppFound := k.applicationKeeper.GetApplication(ctx, appAddress)
if !isAppFound {
err := apptypes.ErrAppNotFound.Wrapf(
"trying to settle a claim for an application that does not exist (which should never happen) with address: %q",
appAddress,
)
return nil, err
}

applicationInitialStakeMap[appAddress] = *app.GetStake()
}

return applicationInitialStakeMap, nil
}
Loading

0 comments on commit ff76430

Please sign in to comment.