Skip to content

Commit

Permalink
Merge pull request #109 from osmosis-labs/slashing_tests
Browse files Browse the repository at this point in the history
Add slashing tests
  • Loading branch information
maurolacy authored Nov 22, 2023
2 parents 3aeaf5a + 5b6af5a commit 5aaeca9
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 10 deletions.
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())
// 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())
// 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

0 comments on commit 5aaeca9

Please sign in to comment.