Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add slashing tests #109

Merged
merged 17 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion demo/app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,10 @@ func GenesisStateWithValSet(
ValidatorSigningInfo: slashingtypes.ValidatorSigningInfo{},
}
}
slashingGenesis := slashingtypes.NewGenesisState(slashingtypes.DefaultParams(), signingInfos, nil)
slashingParams := slashingtypes.DefaultParams()
slashingParams.SlashFractionDowntime = math.LegacyNewDec(1).Quo(math.LegacyNewDec(10))
slashingParams.SlashFractionDoubleSign = math.LegacyNewDec(1).Quo(math.LegacyNewDec(10))
slashingGenesis := slashingtypes.NewGenesisState(slashingParams, signingInfos, nil)
genesisState[slashingtypes.ModuleName] = codec.MustMarshalJSON(slashingGenesis)

// add bonded amount to bonded pool module account
Expand Down
285 changes: 285 additions & 0 deletions tests/e2e/slashing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package e2e

import (
"cosmossdk.io/math"
"encoding/base64"
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)

func TestSlashingScenario1(t *testing.T) {
// Slashing scenario 1:
// https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-1-slashed-delegator-has-free-collateral-on-the-vault
//
// - We use millions instead of unit tokens.
x := setupExampleChains(t)
consumerCli, _, providerCli := setupMeshSecurity(t, x)

// Provider chain
// ==============
// Deposit - A user deposits the vault denom to provide some collateral to their account
execMsg := `{"bond":{}}`
providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000))

// Stake Locally - A user triggers a local staking action to a chosen validator.
myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String()
execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`,
x.ProviderDenom, 190_000_000,
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr))))
providerCli.MustExecVault(execLocalStakingMsg)

assert.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance())

// Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains.
myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address)
myExtValidator1Addr := myExtValidator1.String()
err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 100_000_000))
require.NoError(t, err)
myExtValidator2 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[2].Address)
myExtValidator2Addr := myExtValidator2.String()
err = providerCli.ExecStakeRemote(myExtValidator2Addr, sdk.NewInt64Coin(x.ProviderDenom, 50_000_000))
require.NoError(t, err)

require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Check collateral
require.Equal(t, 200_000_000, providerCli.QueryVaultBalance())
// Check max lien
require.Equal(t, 190_000_000, providerCli.QueryMaxLien())
Copy link
Contributor

Choose a reason for hiding this comment

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

As below, i was expecting only 90% of the staked amount as max lien

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Same explanation as before. This check is pre-slashing, in fact. It's there so that we can see that the amounts don't change after slashing.

// Check slashable amount
require.Equal(t, 34_000_000, providerCli.QuerySlashableAmount())
// Check free collateral
require.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance()) // 200 - max(34, 190) = 200 - 190 = 10

// Consumer chain
// ====================
//
// then delegated amount is not updated before the epoch
consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet

// when an epoch ends, the delegation rebalance is triggered
consumerCli.ExecNewEpoch()

// then the total delegated amount is updated
consumerCli.assertTotalDelegated(math.NewInt(67_500_000)) // 150_000_000 / 2 * (1 - 0.1)

// and the delegated amount is updated for the validators
consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("45")) // 100_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor
consumerCli.assertShare(myExtValidator2, math.LegacyMustNewDecFromStr("22.5")) // 50_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor

ctx := x.ConsumerChain.GetContext()
validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, found)
require.False(t, validator1.IsJailed())
// Off by 1_000_000, because of validator self bond on setup
require.Equal(t, validator1.GetTokens(), sdk.NewInt(46_000_000))
validator2, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator2)
require.True(t, found)
require.False(t, validator2.IsJailed())
// Off by 1_000_000, because of validator self bond on setup
require.Equal(t, validator2.GetTokens(), sdk.NewInt(23_500_000))

// Validator 1 on the Consumer chain is jailed
myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address())
jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp)

x.ConsumerChain.NextBlock()

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(41_400_000)) // 10% slash

// Relay IBC packets to the Provider chain
require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Next block on the Provider chain
x.ProviderChain.NextBlock()

// Check new collateral
require.Equal(t, 190_000_000, providerCli.QueryVaultBalance())
// Check new max lien
require.Equal(t, 190_000_000, providerCli.QueryMaxLien())
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you elaborate how the max lien is calculated? Now that the free amount is consumed, I was expecting the max lien to be 190_000_000 * 0.9 to take the local slashing factor into account

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In this scenario there was free collateral to be slashed.

The max lien is the max of all the liens, and as long as it remains below the collateral (free collateral >= 0), it doesn't need to change.

// Check new slashable amount
require.Equal(t, 33_000_000, providerCli.QuerySlashableAmount())
// Check new free collateral
require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 190 - max(33, 190) = 190 - 190 = 0
}

func TestSlashingScenario2(t *testing.T) {
// Slashing scenario 2:
// https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-2-slashed-delegator-has-no-free-collateral-on-the-vault
//
// - We use millions instead of unit tokens.
x := setupExampleChains(t)
consumerCli, _, providerCli := setupMeshSecurity(t, x)

// Provider chain
// ==============
// Deposit - A user deposits the vault denom to provide some collateral to their account
execMsg := `{"bond":{}}`
providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000))

// Stake Locally - A user triggers a local staking action to a chosen validator.
myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String()
execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`,
x.ProviderDenom, 200_000_000,
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr))))
providerCli.MustExecVault(execLocalStakingMsg)

// Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains.
myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address)
myExtValidator1Addr := myExtValidator1.String()
err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000))
require.NoError(t, err)

require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Check collateral
require.Equal(t, 200_000_000, providerCli.QueryVaultBalance())
// Check max lien
require.Equal(t, 200_000_000, providerCli.QueryMaxLien())
// Check slashable amount
require.Equal(t, 40_000_000, providerCli.QuerySlashableAmount())
// Check free collateral
require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 200 - max(40, 200) = 200 - 200 = 0

// Consumer chain
// ====================
//
// then delegated amount is not updated before the epoch
consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet

// when an epoch ends, the delegation rebalance is triggered
consumerCli.ExecNewEpoch()

// then the total delegated amount is updated
consumerCli.assertTotalDelegated(math.NewInt(90_000_000)) // 200_000_000 / 2 * (1 - 0.1)

// and the delegated amount is updated for the validators
consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("90")) // 200_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor

ctx := x.ConsumerChain.GetContext()
validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, found)
require.False(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(91_000_000))

// Validator 1 on the Consumer chain is jailed
myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address())
jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp)

x.ConsumerChain.NextBlock()

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(81_900_000)) // 10% slash

// Relay IBC packets to the Provider chain
require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Next block on the Provider chain
x.ProviderChain.NextBlock()

// Check new collateral
require.Equal(t, 180_000_000, providerCli.QueryVaultBalance())
// Check new max lien
require.Equal(t, 180_000_000, providerCli.QueryMaxLien())
// Check new slashable amount
require.Equal(t, 36_000_000, providerCli.QuerySlashableAmount())
// Check new free collateral
require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 190 - max(36, 190) = 190 - 190 = 0
}

func TestSlashingScenario3(t *testing.T) {
// Slashing scenario 3:
// https://github.com/osmosis-labs/mesh-security/blob/main/docs/ibc/Slashing.md#scenario-3-slashed-delegator-has-some-free-collateral-on-the-vault
//
// - We use millions instead of unit tokens.
x := setupExampleChains(t)
consumerCli, _, providerCli := setupMeshSecurity(t, x)

// Provider chain
// ==============
// Deposit - A user deposits the vault denom to provide some collateral to their account
execMsg := `{"bond":{}}`
providerCli.MustExecVault(execMsg, sdk.NewInt64Coin(x.ProviderDenom, 200_000_000))

// Stake Locally - A user triggers a local staking action to a chosen validator.
myLocalValidatorAddr := sdk.ValAddress(x.ProviderChain.Vals.Validators[0].Address).String()
execLocalStakingMsg := fmt.Sprintf(`{"stake_local":{"amount": {"denom":%q, "amount":"%d"}, "msg":%q}}`,
x.ProviderDenom, 190_000_000,
base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"validator": "%s"}`, myLocalValidatorAddr))))
providerCli.MustExecVault(execLocalStakingMsg)

// Cross Stake - A user pulls out additional liens on the same collateral "cross staking" it on different chains.
myExtValidator1 := sdk.ValAddress(x.ConsumerChain.Vals.Validators[1].Address)
myExtValidator1Addr := myExtValidator1.String()
err := providerCli.ExecStakeRemote(myExtValidator1Addr, sdk.NewInt64Coin(x.ProviderDenom, 150_000_000))
require.NoError(t, err)

require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Check collateral
require.Equal(t, 200_000_000, providerCli.QueryVaultBalance())
// Check max lien
require.Equal(t, 190_000_000, providerCli.QueryMaxLien())
// Check slashable amount
require.Equal(t, 34_000_000, providerCli.QuerySlashableAmount())
// Check free collateral
require.Equal(t, 10_000_000, providerCli.QueryVaultFreeBalance()) // 200 - max(34, 190) = 200 - 190 = 10

// Consumer chain
// ====================
//
// then delegated amount is not updated before the epoch
consumerCli.assertTotalDelegated(math.ZeroInt()) // ensure nothing cross staked yet

// when an epoch ends, the delegation rebalance is triggered
consumerCli.ExecNewEpoch()

// then the total delegated amount is updated
consumerCli.assertTotalDelegated(math.NewInt(67_500_000)) // 150_000_000 / 2 * (1 - 0.1)

// and the delegated amount is updated for the validators
consumerCli.assertShare(myExtValidator1, math.LegacyMustNewDecFromStr("67.5")) // 150_000_000 / 2 * (1 - 0.1) / 1_000_000 # default sdk factor

ctx := x.ConsumerChain.GetContext()
validator1, found := x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, found)
require.False(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(68_500_000))

// Validator 1 on the Consumer chain is jailed
myExtValidator1ConsAddr := sdk.ConsAddress(x.ConsumerChain.Vals.Validators[1].PubKey.Address())
jailValidator(t, myExtValidator1ConsAddr, x.Coordinator, x.ConsumerChain, x.ConsumerApp)

x.ConsumerChain.NextBlock()

// Assert that the validator's stake has been slashed
// and that the validator has been jailed
validator1, found = x.ConsumerApp.StakingKeeper.GetValidator(ctx, myExtValidator1)
require.True(t, validator1.IsJailed())
require.Equal(t, validator1.GetTokens(), sdk.NewInt(61_700_000)) // 10% slash (plus 50_000 rounding)

// Relay IBC packets to the Provider chain
require.NoError(t, x.Coordinator.RelayAndAckPendingPackets(x.IbcPath))

// Next block on the Provider chain
x.ProviderChain.NextBlock()

// Check new collateral
require.Equal(t, 185_000_000, providerCli.QueryVaultBalance())
// Check new max lien
require.Equal(t, 185_000_000, providerCli.QueryMaxLien())
// Check new slashable amount
require.Equal(t, 32_000_000, providerCli.QuerySlashableAmount())
// Check new free collateral
require.Equal(t, 0, providerCli.QueryVaultFreeBalance()) // 185 - max(32, 185) = 185 - 185 = 0
}
28 changes: 27 additions & 1 deletion tests/e2e/test_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (p *TestProviderClient) BootstrapContracts(connId, portID string) ProviderC
var (
unbondingPeriod = 21 * 24 * 60 * 60 // 21 days - make configurable?
maxLocalSlashing = "0.10"
maxExtSlashing = "0.05"
maxExtSlashing = "0.10"
rewardTokenDenom = sdk.DefaultBondDenom
localTokenDenom = sdk.DefaultBondDenom
)
Expand Down Expand Up @@ -196,6 +196,32 @@ func (p TestProviderClient) QueryVaultFreeBalance() int {
return ParseHighLow(p.t, qRsp["free"]).Low
}

func (p TestProviderClient) QueryVaultBalance() int {
qRsp := p.QueryVault(Query{
"account_details": {"account": p.chain.SenderAccount.GetAddress().String()},
})
require.NotEmpty(p.t, qRsp["bonded"], qRsp)
b, err := strconv.Atoi(qRsp["bonded"].(string))
require.NoError(p.t, err)
return b
}

func (p TestProviderClient) QueryMaxLien() int {
qRsp := p.QueryVault(Query{
"account_details": {"account": p.chain.SenderAccount.GetAddress().String()},
})
require.NotEmpty(p.t, qRsp["max_lien"], qRsp)
return ParseHighLow(p.t, qRsp["max_lien"]).Low
}

func (p TestProviderClient) QuerySlashableAmount() int {
qRsp := p.QueryVault(Query{
"account_details": {"account": p.chain.SenderAccount.GetAddress().String()},
})
require.NotEmpty(p.t, qRsp["total_slashable"], qRsp)
return ParseHighLow(p.t, qRsp["total_slashable"]).Low
}

type TestConsumerClient struct {
t *testing.T
chain *ibctesting.TestChain
Expand Down
Loading
Loading