392 lines
13 KiB
Go
392 lines
13 KiB
Go
package keeper
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
|
"github.com/cosmos/cosmos-sdk/x/staking"
|
|
"github.com/cosmos/cosmos-sdk/x/staking/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"io/ioutil"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
)
|
|
|
|
type StakingInitMsg struct {
|
|
Name string `json:"name"`
|
|
Symbol string `json:"symbol"`
|
|
Decimals uint8 `json:"decimals"`
|
|
Validator sdk.ValAddress `json:"validator"`
|
|
ExitTax sdk.Dec `json:"exit_tax"`
|
|
// MinWithdrawl is uint128 encoded as a string (use sdk.Int?)
|
|
MinWithdrawl string `json:"min_withdrawl"`
|
|
}
|
|
|
|
// StakingHandleMsg is used to encode handle messages
|
|
type StakingHandleMsg struct {
|
|
Transfer *transferPayload `json:"transfer,omitempty"`
|
|
Bond *struct{} `json:"bond,omitempty"`
|
|
Unbond *unbondPayload `json:"unbond,omitempty"`
|
|
Claim *struct{} `json:"claim,omitempty"`
|
|
Reinvest *struct{} `json:"reinvest,omitempty"`
|
|
Change *ownerPayload `json:"change_owner,omitempty"`
|
|
}
|
|
|
|
type transferPayload struct {
|
|
Recipient sdk.Address `json:"recipient"`
|
|
// uint128 encoded as string
|
|
Amount string `json:"amount"`
|
|
}
|
|
|
|
type unbondPayload struct {
|
|
// uint128 encoded as string
|
|
Amount string `json:"amount"`
|
|
}
|
|
|
|
// StakingQueryMsg is used to encode query messages
|
|
type StakingQueryMsg struct {
|
|
Balance *addressQuery `json:"balance,omitempty"`
|
|
Claims *addressQuery `json:"claims,omitempty"`
|
|
TokenInfo *struct{} `json:"token_info,omitempty"`
|
|
Investment *struct{} `json:"investment,omitempty"`
|
|
}
|
|
|
|
type addressQuery struct {
|
|
Address sdk.AccAddress `json:"address"`
|
|
}
|
|
|
|
type BalanceResponse struct {
|
|
Balance string `json:"balance,omitempty"`
|
|
}
|
|
|
|
type ClaimsResponse struct {
|
|
Claims string `json:"claims,omitempty"`
|
|
}
|
|
|
|
type TokenInfoResponse struct {
|
|
Name string `json:"name"`
|
|
Symbol string `json:"symbol"`
|
|
Decimals uint8 `json:"decimals"`
|
|
}
|
|
|
|
type InvestmentResponse struct {
|
|
TokenSupply string `json:"token_supply"`
|
|
StakedTokens sdk.Coin `json:"staked_tokens"`
|
|
NominalValue sdk.Dec `json:"nominal_value"`
|
|
Owner sdk.AccAddress `json:"owner"`
|
|
Validator sdk.ValAddress `json:"validator"`
|
|
ExitTax sdk.Dec `json:"exit_tax"`
|
|
// MinWithdrawl is uint128 encoded as a string (use sdk.Int?)
|
|
MinWithdrawl string `json:"min_withdrawl"`
|
|
}
|
|
|
|
// adds a few validators and returns a list of validators that are registered
|
|
func addValidators(ctx sdk.Context, stakingKeeper staking.Keeper, powers []sdk.Int) []sdk.ValAddress {
|
|
var addrs = make([]sdk.ValAddress, len(powers))
|
|
for i, power := range powers {
|
|
addrs[i] = addValidator(ctx, stakingKeeper, power)
|
|
}
|
|
return addrs
|
|
}
|
|
|
|
// adds a few validators and returns a list of validators that are registered
|
|
func addValidator(ctx sdk.Context, stakingKeeper staking.Keeper, power sdk.Int) sdk.ValAddress {
|
|
_, pub, accAddr := keyPubAddr()
|
|
addr := sdk.ValAddress(accAddr)
|
|
// make it a bonded validator with power stake
|
|
val := types.NewValidator(addr, pub, types.Description{Moniker: fmt.Sprintf("Validator power %s", power)})
|
|
val.Status = sdk.Bonded
|
|
val, _ = val.AddTokensFromDel(power)
|
|
// store it
|
|
stakingKeeper.SetValidator(ctx, val)
|
|
stakingKeeper.SetValidatorByPowerIndex(ctx, val)
|
|
return addr
|
|
}
|
|
|
|
func TestInitializeStaking(t *testing.T) {
|
|
tempDir, err := ioutil.TempDir("", "wasm")
|
|
require.NoError(t, err)
|
|
defer os.RemoveAll(tempDir)
|
|
ctx, accKeeper, stakingKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil)
|
|
|
|
valAddr := addValidator(ctx, stakingKeeper, sdk.NewInt(1234567))
|
|
v, found := stakingKeeper.GetValidator(ctx, valAddr)
|
|
assert.True(t, found)
|
|
assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1234567))
|
|
|
|
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000))
|
|
creator := createFakeFundedAccount(ctx, accKeeper, deposit)
|
|
|
|
// upload staking derivates code
|
|
stakingCode, err := ioutil.ReadFile("./testdata/staking.wasm")
|
|
require.NoError(t, err)
|
|
stakingID, err := keeper.Create(ctx, creator, stakingCode, "", "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(1), stakingID)
|
|
|
|
// register to a valid address
|
|
initMsg := StakingInitMsg{
|
|
Name: "Staking Derivatives",
|
|
Symbol: "DRV",
|
|
Decimals: 0,
|
|
Validator: valAddr,
|
|
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
|
MinWithdrawl: "100",
|
|
}
|
|
initBz, err := json.Marshal(&initMsg)
|
|
require.NoError(t, err)
|
|
|
|
stakingAddr, err := keeper.Instantiate(ctx, stakingID, creator, initBz, "staking derivates - DRV", nil)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, stakingAddr)
|
|
|
|
// nothing spent here
|
|
checkAccount(t, ctx, accKeeper, creator, deposit)
|
|
|
|
// try to register with a validator not on the list and it fails
|
|
_, _, bob := keyPubAddr()
|
|
badInitMsg := StakingInitMsg{
|
|
Name: "Missing Validator",
|
|
Symbol: "MISS",
|
|
Decimals: 0,
|
|
Validator: sdk.ValAddress(bob),
|
|
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
|
MinWithdrawl: "100",
|
|
}
|
|
badBz, err := json.Marshal(&badInitMsg)
|
|
require.NoError(t, err)
|
|
|
|
_, err = keeper.Instantiate(ctx, stakingID, creator, badBz, "missing validator", nil)
|
|
require.Error(t, err)
|
|
|
|
// no changes to bonding shares
|
|
val, _ := stakingKeeper.GetValidator(ctx, valAddr)
|
|
assert.Equal(t, val.GetDelegatorShares(), sdk.NewDec(1234567))
|
|
}
|
|
|
|
type initInfo struct {
|
|
valAddr sdk.ValAddress
|
|
creator sdk.AccAddress
|
|
contractAddr sdk.AccAddress
|
|
|
|
ctx sdk.Context
|
|
accKeeper auth.AccountKeeper
|
|
stakingKeeper staking.Keeper
|
|
wasmKeeper Keeper
|
|
|
|
cleanup func()
|
|
}
|
|
|
|
func initializeStaking(t *testing.T) initInfo {
|
|
tempDir, err := ioutil.TempDir("", "wasm")
|
|
require.NoError(t, err)
|
|
ctx, accKeeper, stakingKeeper, keeper := CreateTestInput(t, false, tempDir, SupportedFeatures, nil, nil)
|
|
|
|
valAddr := addValidator(ctx, stakingKeeper, sdk.NewInt(1234567))
|
|
v, found := stakingKeeper.GetValidator(ctx, valAddr)
|
|
assert.True(t, found)
|
|
assert.Equal(t, v.GetDelegatorShares(), sdk.NewDec(1234567))
|
|
|
|
deposit := sdk.NewCoins(sdk.NewInt64Coin("denom", 100000), sdk.NewInt64Coin("stake", 500000))
|
|
creator := createFakeFundedAccount(ctx, accKeeper, deposit)
|
|
|
|
// upload staking derivates code
|
|
stakingCode, err := ioutil.ReadFile("./testdata/staking.wasm")
|
|
require.NoError(t, err)
|
|
stakingID, err := keeper.Create(ctx, creator, stakingCode, "", "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, uint64(1), stakingID)
|
|
|
|
// register to a valid address
|
|
initMsg := StakingInitMsg{
|
|
Name: "Staking Derivatives",
|
|
Symbol: "DRV",
|
|
Decimals: 0,
|
|
Validator: valAddr,
|
|
ExitTax: sdk.MustNewDecFromStr("0.10"),
|
|
MinWithdrawl: "100",
|
|
}
|
|
initBz, err := json.Marshal(&initMsg)
|
|
require.NoError(t, err)
|
|
|
|
stakingAddr, err := keeper.Instantiate(ctx, stakingID, creator, initBz, "staking derivates - DRV", nil)
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, stakingAddr)
|
|
|
|
return initInfo{
|
|
valAddr: valAddr,
|
|
creator: creator,
|
|
contractAddr: stakingAddr,
|
|
ctx: ctx,
|
|
accKeeper: accKeeper,
|
|
stakingKeeper: stakingKeeper,
|
|
wasmKeeper: keeper,
|
|
cleanup: func() { os.RemoveAll(tempDir) },
|
|
}
|
|
}
|
|
|
|
func TestBonding(t *testing.T) {
|
|
initInfo := initializeStaking(t)
|
|
defer initInfo.cleanup()
|
|
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
|
keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper
|
|
|
|
// initial checks of bonding state
|
|
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
|
require.True(t, found)
|
|
initPower := val.GetDelegatorShares()
|
|
|
|
// bob has 160k, putting 80k into the contract
|
|
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000))
|
|
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000))
|
|
bob := createFakeFundedAccount(ctx, accKeeper, full)
|
|
|
|
// check contract state before
|
|
assertBalance(t, ctx, keeper, contractAddr, bob, "0")
|
|
assertClaims(t, ctx, keeper, contractAddr, bob, "0")
|
|
assertSupply(t, ctx, keeper, contractAddr, "0", sdk.NewInt64Coin("stake", 0))
|
|
|
|
bond := StakingHandleMsg{
|
|
Bond: &struct{}{},
|
|
}
|
|
bondBz, err := json.Marshal(bond)
|
|
require.NoError(t, err)
|
|
_, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
|
require.NoError(t, err)
|
|
|
|
// check some account values - the money is on neither account (cuz it is bonded)
|
|
checkAccount(t, ctx, accKeeper, contractAddr, sdk.Coins{})
|
|
checkAccount(t, ctx, accKeeper, bob, funds)
|
|
|
|
// make sure the proper number of tokens have been bonded
|
|
val, _ = stakingKeeper.GetValidator(ctx, valAddr)
|
|
finalPower := val.GetDelegatorShares()
|
|
assert.Equal(t, sdk.NewInt(80000), finalPower.Sub(initPower).TruncateInt())
|
|
|
|
// check the delegation itself
|
|
d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr)
|
|
require.True(t, found)
|
|
assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("80000"))
|
|
|
|
// check we have the desired balance
|
|
assertBalance(t, ctx, keeper, contractAddr, bob, "80000")
|
|
assertClaims(t, ctx, keeper, contractAddr, bob, "0")
|
|
assertSupply(t, ctx, keeper, contractAddr, "80000", sdk.NewInt64Coin("stake", 80000))
|
|
}
|
|
|
|
func TestUnbonding(t *testing.T) {
|
|
initInfo := initializeStaking(t)
|
|
defer initInfo.cleanup()
|
|
ctx, valAddr, contractAddr := initInfo.ctx, initInfo.valAddr, initInfo.contractAddr
|
|
keeper, stakingKeeper, accKeeper := initInfo.wasmKeeper, initInfo.stakingKeeper, initInfo.accKeeper
|
|
|
|
// initial checks of bonding state
|
|
val, found := stakingKeeper.GetValidator(ctx, valAddr)
|
|
require.True(t, found)
|
|
initPower := val.GetDelegatorShares()
|
|
|
|
// bob has 160k, putting 80k into the contract
|
|
full := sdk.NewCoins(sdk.NewInt64Coin("stake", 160000))
|
|
funds := sdk.NewCoins(sdk.NewInt64Coin("stake", 80000))
|
|
bob := createFakeFundedAccount(ctx, accKeeper, full)
|
|
|
|
bond := StakingHandleMsg{
|
|
Bond: &struct{}{},
|
|
}
|
|
bondBz, err := json.Marshal(bond)
|
|
require.NoError(t, err)
|
|
_, err = keeper.Execute(ctx, contractAddr, bob, bondBz, funds)
|
|
require.NoError(t, err)
|
|
|
|
// update height a bit
|
|
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 5)
|
|
|
|
// now unbond 30k - note that 3k (10%) goes to the owner as a tax, 27k unbonded and available as claims
|
|
unbond := StakingHandleMsg{
|
|
Unbond: &unbondPayload{
|
|
Amount: "30000",
|
|
},
|
|
}
|
|
unbondBz, err := json.Marshal(unbond)
|
|
require.NoError(t, err)
|
|
_, err = keeper.Execute(ctx, contractAddr, bob, unbondBz, nil)
|
|
require.NoError(t, err)
|
|
|
|
// check some account values - the money is on neither account (cuz it is bonded)
|
|
// Note: why is this immediate? just test setup?
|
|
checkAccount(t, ctx, accKeeper, contractAddr, sdk.Coins{})
|
|
checkAccount(t, ctx, accKeeper, bob, funds)
|
|
|
|
// make sure the proper number of tokens have been bonded (80k - 27k = 53k)
|
|
val, _ = stakingKeeper.GetValidator(ctx, valAddr)
|
|
finalPower := val.GetDelegatorShares()
|
|
assert.Equal(t, sdk.NewInt(53000), finalPower.Sub(initPower).TruncateInt(), finalPower.String())
|
|
|
|
// check the delegation itself
|
|
d, found := stakingKeeper.GetDelegation(ctx, contractAddr, valAddr)
|
|
require.True(t, found)
|
|
assert.Equal(t, d.Shares, sdk.MustNewDecFromStr("53000"))
|
|
|
|
// check there is unbonding in progress
|
|
un, found := stakingKeeper.GetUnbondingDelegation(ctx, contractAddr, valAddr)
|
|
require.True(t, found)
|
|
require.Equal(t, 1, len(un.Entries))
|
|
assert.Equal(t, "27000", un.Entries[0].Balance.String())
|
|
|
|
// check we have the desired balance
|
|
assertBalance(t, ctx, keeper, contractAddr, bob, "50000")
|
|
assertBalance(t, ctx, keeper, contractAddr, initInfo.creator, "3000")
|
|
assertClaims(t, ctx, keeper, contractAddr, bob, "27000")
|
|
assertSupply(t, ctx, keeper, contractAddr, "53000", sdk.NewInt64Coin("stake", 53000))
|
|
|
|
}
|
|
|
|
func assertBalance(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) {
|
|
query := StakingQueryMsg{
|
|
Balance: &addressQuery{
|
|
Address: addr,
|
|
},
|
|
}
|
|
queryBz, err := json.Marshal(query)
|
|
require.NoError(t, err)
|
|
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
|
require.NoError(t, err)
|
|
var balance BalanceResponse
|
|
err = json.Unmarshal(res, &balance)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, balance.Balance)
|
|
}
|
|
|
|
func assertClaims(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, addr sdk.AccAddress, expected string) {
|
|
query := StakingQueryMsg{
|
|
Claims: &addressQuery{
|
|
Address: addr,
|
|
},
|
|
}
|
|
queryBz, err := json.Marshal(query)
|
|
require.NoError(t, err)
|
|
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
|
require.NoError(t, err)
|
|
var claims ClaimsResponse
|
|
err = json.Unmarshal(res, &claims)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expected, claims.Claims)
|
|
}
|
|
|
|
func assertSupply(t *testing.T, ctx sdk.Context, keeper Keeper, contract sdk.AccAddress, expectedIssued string, expectedBonded sdk.Coin) {
|
|
query := StakingQueryMsg{Investment: &struct{}{}}
|
|
queryBz, err := json.Marshal(query)
|
|
require.NoError(t, err)
|
|
res, err := keeper.QuerySmart(ctx, contract, queryBz)
|
|
require.NoError(t, err)
|
|
var invest InvestmentResponse
|
|
err = json.Unmarshal(res, &invest)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, expectedIssued, invest.TokenSupply)
|
|
assert.Equal(t, expectedBonded, invest.StakedTokens)
|
|
}
|