From 80d88c3a4cfbbfe286865316071c2b5a2f79b915 Mon Sep 17 00:00:00 2001 From: rigelrozanski Date: Fri, 23 Feb 2018 23:57:31 +0000 Subject: [PATCH] porting staking, wip wip --- x/stake/errors.go | 103 ++++++++ x/stake/handler.go | 520 ++++++++++++++++++++++++++++++++++++++++ x/stake/handler_test.go | 328 +++++++++++++++++++++++++ x/stake/tick.go | 74 ++++++ x/stake/tick_test.go | 116 +++++++++ 5 files changed, 1141 insertions(+) create mode 100644 x/stake/errors.go create mode 100644 x/stake/handler.go create mode 100644 x/stake/handler_test.go create mode 100644 x/stake/tick.go create mode 100644 x/stake/tick_test.go diff --git a/x/stake/errors.go b/x/stake/errors.go new file mode 100644 index 0000000000..1f5ab2fc61 --- /dev/null +++ b/x/stake/errors.go @@ -0,0 +1,103 @@ +// nolint +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type CodeType = sdk.CodeType + +const ( + // Gaia errors reserve 200 ~ 299. + CodeInvalidValidator CodeType = 201 + CodeInvalidCandidate CodeType = 202 + CodeInvalidBond CodeType = 203 + CodeInvalidInput CodeType = 204 + CodeUnauthorized CodeType = sdk.CodeUnauthorized + CodeInternal CodeType = sdk.CodeInternal + CodeUnknownRequest CodeType = sdk.CodeUnknownRequest +) + +// NOTE: Don't stringer this, we'll put better messages in later. +func codeToDefaultMsg(code CodeType) string { + switch code { + case CodeInvalidValidator: + return "Invalid Validator" + case CodeInvalidCandidate: + return "Invalid Candidate" + case CodeInvalidBond: + return "Invalid Bond" + case CodeInvalidInput: + return "Invalid Input" + case CodeUnauthorized: + return "Unauthorized" + case CodeInternal: + return "Internal Error" + case CodeUnknownRequest: + return "Unknown request" + default: + return sdk.CodeToDefaultMsg(code) + } +} + +//---------------------------------------- +// Error constructors + +func ErrCandidateEmpty() error { + return newError(CodeInvalidValidator, "Cannot bond to an empty candidate") +} +func ErrBadBondingDenom() error { + return newError(CodeInvalidValidator, "Invalid coin denomination") +} +func ErrBadBondingAmount() error { + return newError(CodeInvalidValidator, "Amount must be > 0") +} +func ErrNoBondingAcct() error { + return newError(CodeInvalidValidator, "No bond account for this (address, validator) pair") +} +func ErrCommissionNegative() error { + return newError(CodeInvalidValidator, "Commission must be positive") +} +func ErrCommissionHuge() error { + return newError(CodeInvalidValidator, "Commission cannot be more than 100%") +} +func ErrBadValidatorAddr() error { + return newError(CodeInvalidValidator, "Validator does not exist for that address") +} +func ErrCandidateExistsAddr() error { + return newError(CodeInvalidValidator, "Candidate already exist, cannot re-declare candidacy") +} +func ErrMissingSignature() error { + return newError(CodeInvalidValidator, "Missing signature") +} +func ErrBondNotNominated() error { + return newError(CodeInvalidValidator, "Cannot bond to non-nominated account") +} +func ErrNoCandidateForAddress() error { + return newError(CodeInvalidValidator, "Validator does not exist for that address") +} +func ErrNoDelegatorForAddress() error { + return newError(CodeInvalidValidator, "Delegator does not contain validator bond") +} +func ErrInsufficientFunds() error { + return newError(CodeInvalidValidator, "Insufficient bond shares") +} +func ErrBadRemoveValidator() error { + return newError(CodeInvalidValidator, "Error removing validator") +} + +//---------------------------------------- + +// TODO group with code from x/bank/errors.go + +func msgOrDefaultMsg(msg string, code CodeType) string { + if msg != "" { + return msg + } + return codeToDefaultMsg(code) +} + +func newError(code CodeType, msg string) sdk.Error { + msg = msgOrDefaultMsg(msg, code) + return sdk.NewError(code, msg) +} diff --git a/x/stake/handler.go b/x/stake/handler.go new file mode 100644 index 0000000000..e03a31b043 --- /dev/null +++ b/x/stake/handler.go @@ -0,0 +1,520 @@ +package stake + +import ( + "fmt" + "strconv" + + "github.com/spf13/viper" + "github.com/tendermint/tmlibs/log" + "github.com/tendermint/tmlibs/rational" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + coin "github.com/cosmos/cosmos-sdk/x/bank" // XXX fix +) + +// nolint +const stakingModuleName = "stake" + +// Name is the name of the modules. +func Name() string { + return stakingModuleName +} + +//_______________________________________________________________________ + +// DelegatedProofOfStake - interface to enforce delegation stake +type delegatedProofOfStake interface { + declareCandidacy(TxDeclareCandidacy) error + editCandidacy(TxEditCandidacy) error + delegate(TxDelegate) error + unbond(TxUnbond) error +} + +type coinSend interface { + transferFn(sender, receiver sdk.Actor, coins coin.Coins) error +} + +//_______________________________________________________________________ + +// Handler - the transaction processing handler +type Handler struct { +} + +// NewHandler returns a new Handler with the default Params +func NewHandler() Handler { + return Handler{} +} + +// Name - return stake namespace +func (Handler) Name() string { + return stakingModuleName +} + +// InitState - set genesis parameters for staking +func (h Handler) InitState(l log.Logger, store types.KVStore, + module, key, value string, cb sdk.InitStater) (log string, err error) { + return "", h.initState(module, key, value, store) +} + +// separated for testing +func (Handler) initState(module, key, value string, store types.KVStore) error { + if module != stakingModuleName { + return sdk.ErrUnknownModule(module) + } + + params := loadParams(store) + switch key { + case "allowed_bond_denom": + params.AllowedBondDenom = value + case "max_vals", + "gas_bond", + "gas_unbond": + + // TODO: enforce non-negative integers in input + i, err := strconv.Atoi(value) + if err != nil { + return fmt.Errorf("input must be integer, Error: %v", err.Error()) + } + + switch key { + case "max_vals": + params.MaxVals = uint16(i) + case "gas_bond": + params.GasDelegate = int64(i) + case "gas_unbound": + params.GasUnbond = int64(i) + } + default: + return sdk.ErrUnknownKey(key) + } + + saveParams(store, params) + return nil +} + +// CheckTx checks if the tx is properly structured +func (h Handler) CheckTx(ctx sdk.Context, store types.KVStore, + tx sdk.Tx, _ sdk.Checker) (res sdk.CheckResult, err error) { + + err = tx.ValidateBasic() + if err != nil { + return res, err + } + + // get the sender + sender, err := getTxSender(ctx) + if err != nil { + return res, err + } + + params := loadParams(store) + + // create the new checker object to + checker := check{ + store: store, + sender: sender, + } + + // return the fee for each tx type + switch txInner := tx.Unwrap().(type) { + case TxDeclareCandidacy: + return sdk.NewCheck(params.GasDeclareCandidacy, ""), + checker.declareCandidacy(txInner) + case TxEditCandidacy: + return sdk.NewCheck(params.GasEditCandidacy, ""), + checker.editCandidacy(txInner) + case TxDelegate: + return sdk.NewCheck(params.GasDelegate, ""), + checker.delegate(txInner) + case TxUnbond: + return sdk.NewCheck(params.GasUnbond, ""), + checker.unbond(txInner) + } + + return res, sdk.ErrUnknownTxType(tx) +} + +// DeliverTx executes the tx if valid +func (h Handler) DeliverTx(ctx sdk.Context, store types.KVStore, + tx sdk.Tx, dispatch sdk.Deliver) (res sdk.DeliverResult, err error) { + + // TODO: remove redundancy + // also we don't need to check the res - gas is already deducted in sdk + _, err = h.CheckTx(ctx, store, tx, nil) + if err != nil { + return + } + + sender, err := getTxSender(ctx) + if err != nil { + return + } + + params := loadParams(store) + deliverer := deliver{ + store: store, + sender: sender, + params: params, + transfer: coinSender{ + store: store, + dispatch: dispatch, + ctx: ctx, + }.transferFn, + } + + // Run the transaction + switch _tx := tx.Unwrap().(type) { + case TxDeclareCandidacy: + res.GasUsed = params.GasDeclareCandidacy + return res, deliverer.declareCandidacy(_tx) + case TxEditCandidacy: + res.GasUsed = params.GasEditCandidacy + return res, deliverer.editCandidacy(_tx) + case TxDelegate: + res.GasUsed = params.GasDelegate + return res, deliverer.delegate(_tx) + case TxUnbond: + //context with hold account permissions + params := loadParams(store) + res.GasUsed = params.GasUnbond + ctx2 := ctx.WithPermissions(params.HoldBonded) + deliverer.transfer = coinSender{ + store: store, + dispatch: dispatch, + ctx: ctx2, + }.transferFn + return res, deliverer.unbond(_tx) + } + return +} + +// get the sender from the ctx and ensure it matches the tx pubkey +func getTxSender(ctx sdk.Context) (sender sdk.Actor, err error) { + senders := ctx.GetPermissions("", auth.NameSigs) + if len(senders) != 1 { + return sender, ErrMissingSignature() + } + return senders[0], nil +} + +//_______________________________________________________________________ + +type coinSender struct { + store types.KVStore + dispatch sdk.Deliver + ctx sdk.Context +} + +var _ coinSend = coinSender{} // enforce interface at compile time + +func (c coinSender) transferFn(sender, receiver sdk.Actor, coins coin.Coins) error { + send := coin.NewSendOneTx(sender, receiver, coins) + + // If the deduction fails (too high), abort the command + _, err := c.dispatch.DeliverTx(c.ctx, c.store, send) + return err +} + +//_____________________________________________________________________ + +type check struct { + store types.KVStore + sender sdk.Actor +} + +var _ delegatedProofOfStake = check{} // enforce interface at compile time + +func (c check) declareCandidacy(tx TxDeclareCandidacy) error { + + // check to see if the pubkey or sender has been registered before + candidate := loadCandidate(c.store, tx.PubKey) + if candidate != nil { + return fmt.Errorf("cannot bond to pubkey which is already declared candidacy"+ + " PubKey %v already registered with %v candidate address", + candidate.PubKey, candidate.Owner) + } + + return checkDenom(tx.BondUpdate, c.store) +} + +func (c check) editCandidacy(tx TxEditCandidacy) error { + + // candidate must already be registered + candidate := loadCandidate(c.store, tx.PubKey) + if candidate == nil { // does PubKey exist + return fmt.Errorf("cannot delegate to non-existant PubKey %v", tx.PubKey) + } + return nil +} + +func (c check) delegate(tx TxDelegate) error { + + candidate := loadCandidate(c.store, tx.PubKey) + if candidate == nil { // does PubKey exist + return fmt.Errorf("cannot delegate to non-existant PubKey %v", tx.PubKey) + } + return checkDenom(tx.BondUpdate, c.store) +} + +func (c check) unbond(tx TxUnbond) error { + + // check if bond has any shares in it unbond + bond := loadDelegatorBond(c.store, c.sender, tx.PubKey) + sharesStr := viper.GetString(tx.Shares) + if bond.Shares.LT(rational.Zero) { // bond shares < tx shares + return fmt.Errorf("no shares in account to unbond") + } + + // if shares set to maximum shares then we're good + if sharesStr == "MAX" { + return nil + } + + // test getting rational number from decimal provided + shares, err := rational.NewFromDecimal(sharesStr) + if err != nil { + return err + } + + // test that there are enough shares to unbond + if bond.Shares.LT(shares) { + return fmt.Errorf("not enough bond shares to unbond, have %v, trying to unbond %v", + bond.Shares, tx.Shares) + } + return nil +} + +func checkDenom(tx BondUpdate, store types.KVStore) error { + if tx.Bond.Denom != loadParams(store).AllowedBondDenom { + return fmt.Errorf("Invalid coin denomination") + } + return nil +} + +//_____________________________________________________________________ + +type deliver struct { + store types.KVStore + sender sdk.Actor + params Params + gs *GlobalState + transfer transferFn +} + +type transferFn func(sender, receiver sdk.Actor, coins coin.Coins) error + +var _ delegatedProofOfStake = deliver{} // enforce interface at compile time + +//_____________________________________________________________________ +// deliver helper functions + +// TODO move from deliver with new SDK should only be dependant on store to send coins in NEW SDK + +// move a candidates asset pool from bonded to unbonded pool +func (d deliver) bondedToUnbondedPool(candidate *Candidate) error { + + // replace bonded shares with unbonded shares + tokens := d.gs.removeSharesBonded(candidate.Assets) + candidate.Assets = d.gs.addTokensUnbonded(tokens) + candidate.Status = Unbonded + + return d.transfer(d.params.HoldBonded, d.params.HoldUnbonded, + coin.Coins{{d.params.AllowedBondDenom, tokens}}) +} + +// move a candidates asset pool from unbonded to bonded pool +func (d deliver) unbondedToBondedPool(candidate *Candidate) error { + + // replace bonded shares with unbonded shares + tokens := d.gs.removeSharesUnbonded(candidate.Assets) + candidate.Assets = d.gs.addTokensBonded(tokens) + candidate.Status = Bonded + + return d.transfer(d.params.HoldUnbonded, d.params.HoldBonded, + coin.Coins{{d.params.AllowedBondDenom, tokens}}) +} + +//_____________________________________________________________________ + +// These functions assume everything has been authenticated, +// now we just perform action and save +func (d deliver) declareCandidacy(tx TxDeclareCandidacy) error { + + // create and save the empty candidate + bond := loadCandidate(d.store, tx.PubKey) + if bond != nil { + return ErrCandidateExistsAddr() + } + candidate := NewCandidate(tx.PubKey, d.sender, tx.Description) + saveCandidate(d.store, candidate) + + // move coins from the d.sender account to a (self-bond) delegator account + // the candidate account and global shares are updated within here + txDelegate := TxDelegate{tx.BondUpdate} + return d.delegateWithCandidate(txDelegate, candidate) +} + +func (d deliver) editCandidacy(tx TxEditCandidacy) error { + + // Get the pubKey bond account + candidate := loadCandidate(d.store, tx.PubKey) + if candidate == nil { + return ErrBondNotNominated() + } + if candidate.Status == Unbonded { //candidate has been withdrawn + return ErrBondNotNominated() + } + + //check and edit any of the editable terms + if tx.Description.Moniker != "" { + candidate.Description.Moniker = tx.Description.Moniker + } + if tx.Description.Identity != "" { + candidate.Description.Identity = tx.Description.Identity + } + if tx.Description.Website != "" { + candidate.Description.Website = tx.Description.Website + } + if tx.Description.Details != "" { + candidate.Description.Details = tx.Description.Details + } + + saveCandidate(d.store, candidate) + return nil +} + +func (d deliver) delegate(tx TxDelegate) error { + // Get the pubKey bond account + candidate := loadCandidate(d.store, tx.PubKey) + if candidate == nil { + return ErrBondNotNominated() + } + return d.delegateWithCandidate(tx, candidate) +} + +func (d deliver) delegateWithCandidate(tx TxDelegate, candidate *Candidate) error { + + if candidate.Status == Revoked { //candidate has been withdrawn + return ErrBondNotNominated() + } + + var poolAccount sdk.Actor + if candidate.Status == Bonded { + poolAccount = d.params.HoldBonded + } else { + poolAccount = d.params.HoldUnbonded + } + + // TODO maybe refactor into GlobalState.addBondedTokens(), maybe with new SDK + // Move coins from the delegator account to the bonded pool account + err := d.transfer(d.sender, poolAccount, coin.Coins{tx.Bond}) + if err != nil { + return err + } + + // Get or create the delegator bond + bond := loadDelegatorBond(d.store, d.sender, tx.PubKey) + if bond == nil { + bond = &DelegatorBond{ + PubKey: tx.PubKey, + Shares: rational.Zero, + } + } + + // Account new shares, save + bond.Shares = bond.Shares.Add(candidate.addTokens(tx.Bond.Amount, d.gs)) + saveCandidate(d.store, candidate) + saveDelegatorBond(d.store, d.sender, bond) + saveGlobalState(d.store, d.gs) + return nil +} + +func (d deliver) unbond(tx TxUnbond) error { + + // get delegator bond + bond := loadDelegatorBond(d.store, d.sender, tx.PubKey) + if bond == nil { + return ErrNoDelegatorForAddress() + } + + // retrieve the amount of bonds to remove (TODO remove redundancy already serialized) + var shares rational.Rat + if tx.Shares == "MAX" { + shares = bond.Shares + } else { + var err error + shares, err = rational.NewFromDecimal(tx.Shares) + if err != nil { + return err + } + } + + // subtract bond tokens from delegator bond + if bond.Shares.LT(shares) { // bond shares < tx shares + return ErrInsufficientFunds() + } + bond.Shares = bond.Shares.Sub(shares) + + // get pubKey candidate + candidate := loadCandidate(d.store, tx.PubKey) + if candidate == nil { + return ErrNoCandidateForAddress() + } + + revokeCandidacy := false + if bond.Shares.IsZero() { + + // if the bond is the owner of the candidate then + // trigger a revoke candidacy + if d.sender.Equals(candidate.Owner) && + candidate.Status != Revoked { + revokeCandidacy = true + } + + // remove the bond + removeDelegatorBond(d.store, d.sender, tx.PubKey) + } else { + saveDelegatorBond(d.store, d.sender, bond) + } + + // transfer coins back to account + var poolAccount sdk.Actor + if candidate.Status == Bonded { + poolAccount = d.params.HoldBonded + } else { + poolAccount = d.params.HoldUnbonded + } + + returnCoins := candidate.removeShares(shares, d.gs) + err := d.transfer(poolAccount, d.sender, + coin.Coins{{d.params.AllowedBondDenom, returnCoins}}) + if err != nil { + return err + } + + // lastly if an revoke candidate if necessary + if revokeCandidacy { + + // change the share types to unbonded if they were not already + if candidate.Status == Bonded { + err = d.bondedToUnbondedPool(candidate) + if err != nil { + return err + } + } + + // lastly update the status + candidate.Status = Revoked + } + + // deduct shares from the candidate and save + if candidate.Liabilities.IsZero() { + removeCandidate(d.store, tx.PubKey) + } else { + saveCandidate(d.store, candidate) + } + + saveGlobalState(d.store, d.gs) + return nil +} diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go new file mode 100644 index 0000000000..b6ea3beaa3 --- /dev/null +++ b/x/stake/handler_test.go @@ -0,0 +1,328 @@ +package stake + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tmlibs/rational" + + sdk "github.com/cosmos/cosmos-sdk/types" + coin "github.com/cosmos/cosmos-sdk/x/bank" // XXX fix +) + +//______________________________________________________________________ + +// dummy transfer functions, represents store operations on account balances + +type testCoinSender struct { + store map[string]int64 +} + +var _ coinSend = testCoinSender{} // enforce interface at compile time + +func (c testCoinSender) transferFn(sender, receiver sdk.Actor, coins coin.Coins) error { + c.store[string(sender.Address)] -= coins[0].Amount + c.store[string(receiver.Address)] += coins[0].Amount + return nil +} + +//______________________________________________________________________ + +func initAccounts(n int, amount int64) ([]sdk.Actor, map[string]int64) { + accStore := map[string]int64{} + senders := newActors(n) + for _, sender := range senders { + accStore[string(sender.Address)] = amount + } + return senders, accStore +} + +func newTxDeclareCandidacy(amt int64, pubKey crypto.PubKey) TxDeclareCandidacy { + return TxDeclareCandidacy{ + BondUpdate{ + PubKey: pubKey, + Bond: coin.Coin{"fermion", amt}, + }, + Description{}, + } +} + +func newTxDelegate(amt int64, pubKey crypto.PubKey) TxDelegate { + return TxDelegate{BondUpdate{ + PubKey: pubKey, + Bond: coin.Coin{"fermion", amt}, + }} +} + +func newTxUnbond(shares string, pubKey crypto.PubKey) TxUnbond { + return TxUnbond{ + PubKey: pubKey, + Shares: shares, + } +} + +func paramsNoInflation() Params { + return Params{ + HoldBonded: sdk.NewActor(stakingModuleName, []byte("77777777777777777777777777777777")), + HoldUnbonded: sdk.NewActor(stakingModuleName, []byte("88888888888888888888888888888888")), + InflationRateChange: rational.Zero, + InflationMax: rational.Zero, + InflationMin: rational.Zero, + GoalBonded: rational.New(67, 100), + MaxVals: 100, + AllowedBondDenom: "fermion", + GasDeclareCandidacy: 20, + GasEditCandidacy: 20, + GasDelegate: 20, + GasUnbond: 20, + } +} + +func newDeliver(t, sender sdk.Actor, accStore map[string]int64) deliver { + store := initTestStore() + params := paramsNoInflation() + saveParams(store, params) + return deliver{ + store: store, + sender: sender, + params: params, + gs: loadGlobalState(store), + transfer: testCoinSender{accStore}.transferFn, + } +} + +func TestDuplicatesTxDeclareCandidacy(t *testing.T) { + senders, accStore := initAccounts(2, 1000) // for accounts + + deliverer := newDeliver(t, senders[0], accStore) + checker := check{ + store: deliverer.store, + sender: senders[0], + } + + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(t, got, "expected no error on runTxDeclareCandidacy") + + // one sender can bond to two different pubKeys + txDeclareCandidacy.PubKey = pks[1] + err := checker.declareCandidacy(txDeclareCandidacy) + assert.Nil(t, err, "didn't expected error on checkTx") + + // two senders cant bond to the same pubkey + checker.sender = senders[1] + txDeclareCandidacy.PubKey = pks[0] + err = checker.declareCandidacy(txDeclareCandidacy) + assert.NotNil(t, err, "expected error on checkTx") +} + +func TestIncrementsTxDelegate(t *testing.T) { + initSender := int64(1000) + senders, accStore := initAccounts(1, initSender) // for accounts + deliverer := newDeliver(t, senders[0], accStore) + + // first declare candidacy + bondAmount := int64(10) + txDeclareCandidacy := newTxDeclareCandidacy(bondAmount, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(t, got, "expected declare candidacy tx to be ok, got %v", got) + expectedBond := bondAmount // 1 since we send 1 at the start of loop, + + // just send the same txbond multiple times + holder := deliverer.params.HoldUnbonded // XXX this should be HoldBonded, new SDK updates + txDelegate := newTxDelegate(bondAmount, pks[0]) + for i := 0; i < 5; i++ { + got := deliverer.delegate(txDelegate) + assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the accounts and the bond account have the appropriate values + candidates := loadCandidates(deliverer.store) + expectedBond += bondAmount + expectedSender := initSender - expectedBond + gotBonded := candidates[0].Liabilities.Evaluate() + gotHolder := accStore[string(holder.Address)] + gotSender := accStore[string(deliverer.sender.Address)] + assert.Equal(t, expectedBond, gotBonded, "i: %v, %v, %v", i, expectedBond, gotBonded) + assert.Equal(t, expectedBond, gotHolder, "i: %v, %v, %v", i, expectedBond, gotHolder) + assert.Equal(t, expectedSender, gotSender, "i: %v, %v, %v", i, expectedSender, gotSender) + } +} + +func TestIncrementsTxUnbond(t *testing.T) { + initSender := int64(0) + senders, accStore := initAccounts(1, initSender) // for accounts + deliverer := newDeliver(t, senders[0], accStore) + + // set initial bond + initBond := int64(1000) + accStore[string(deliverer.sender.Address)] = initBond + got := deliverer.declareCandidacy(newTxDeclareCandidacy(initBond, pks[0])) + assert.NoError(t, got, "expected initial bond tx to be ok, got %v", got) + + // just send the same txunbond multiple times + holder := deliverer.params.HoldUnbonded // XXX new SDK, this should be HoldBonded + + // XXX use decimals here + unbondShares, unbondSharesStr := int64(10), "10" + txUndelegate := newTxUnbond(unbondSharesStr, pks[0]) + nUnbonds := 5 + for i := 0; i < nUnbonds; i++ { + got := deliverer.unbond(txUndelegate) + assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the accounts and the bond account have the appropriate values + candidates := loadCandidates(deliverer.store) + expectedBond := initBond - int64(i+1)*unbondShares // +1 since we send 1 at the start of loop + expectedSender := initSender + (initBond - expectedBond) + gotBonded := candidates[0].Liabilities.Evaluate() + gotHolder := accStore[string(holder.Address)] + gotSender := accStore[string(deliverer.sender.Address)] + + assert.Equal(t, expectedBond, gotBonded, "%v, %v", expectedBond, gotBonded) + assert.Equal(t, expectedBond, gotHolder, "%v, %v", expectedBond, gotHolder) + assert.Equal(t, expectedSender, gotSender, "%v, %v", expectedSender, gotSender) + } + + // these are more than we have bonded now + errorCases := []int64{ + //1<<64 - 1, // more than int64 + //1<<63 + 1, // more than int64 + 1<<63 - 1, + 1 << 31, + initBond, + } + for _, c := range errorCases { + unbondShares := strconv.Itoa(int(c)) + txUndelegate := newTxUnbond(unbondShares, pks[0]) + got = deliverer.unbond(txUndelegate) + assert.Error(t, got, "expected unbond tx to fail") + } + + leftBonded := initBond - unbondShares*int64(nUnbonds) + + // should be unable to unbond one more than we have + txUndelegate = newTxUnbond(strconv.Itoa(int(leftBonded)+1), pks[0]) + got = deliverer.unbond(txUndelegate) + assert.Error(t, got, "expected unbond tx to fail") + + // should be able to unbond just what we have + txUndelegate = newTxUnbond(strconv.Itoa(int(leftBonded)), pks[0]) + got = deliverer.unbond(txUndelegate) + assert.NoError(t, got, "expected unbond tx to pass") +} + +func TestMultipleTxDeclareCandidacy(t *testing.T) { + initSender := int64(1000) + senders, accStore := initAccounts(3, initSender) + pubKeys := []crypto.PubKey{pks[0], pks[1], pks[2]} + deliverer := newDeliver(t, senders[0], accStore) + + // bond them all + for i, sender := range senders { + txDeclareCandidacy := newTxDeclareCandidacy(10, pubKeys[i]) + deliverer.sender = sender + got := deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the account is bonded + candidates := loadCandidates(deliverer.store) + val := candidates[i] + balanceGot, balanceExpd := accStore[string(val.Owner.Address)], initSender-10 + assert.Equal(t, i+1, len(candidates), "expected %d candidates got %d, candidates: %v", i+1, len(candidates), candidates) + assert.Equal(t, 10, int(val.Liabilities.Evaluate()), "expected %d shares, got %d", 10, val.Liabilities) + assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot) + } + + // unbond them all + for i, sender := range senders { + candidatePre := loadCandidate(deliverer.store, pubKeys[i]) + txUndelegate := newTxUnbond("10", pubKeys[i]) + deliverer.sender = sender + got := deliverer.unbond(txUndelegate) + assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the account is unbonded + candidates := loadCandidates(deliverer.store) + assert.Equal(t, len(senders)-(i+1), len(candidates), "expected %d candidates got %d", len(senders)-(i+1), len(candidates)) + + candidatePost := loadCandidate(deliverer.store, pubKeys[i]) + balanceGot, balanceExpd := accStore[string(candidatePre.Owner.Address)], initSender + assert.Nil(t, candidatePost, "expected nil candidate retrieve, got %d", 0, candidatePost) + assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot) + } +} + +func TestMultipleTxDelegate(t *testing.T) { + accounts, accStore := initAccounts(3, 1000) + sender, delegators := accounts[0], accounts[1:] + deliverer := newDeliver(t, sender, accStore) + + //first make a candidate + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + require.NoError(t, got, "expected tx to be ok, got %v", got) + + // delegate multiple parties + for i, delegator := range delegators { + txDelegate := newTxDelegate(10, pks[0]) + deliverer.sender = delegator + got := deliverer.delegate(txDelegate) + require.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the account is bonded + bond := loadDelegatorBond(deliverer.store, delegator, pks[0]) + assert.NotNil(t, bond, "expected delegatee bond %d to exist", bond) + } + + // unbond them all + for i, delegator := range delegators { + txUndelegate := newTxUnbond("10", pks[0]) + deliverer.sender = delegator + got := deliverer.unbond(txUndelegate) + require.NoError(t, got, "expected tx %d to be ok, got %v", i, got) + + //Check that the account is unbonded + bond := loadDelegatorBond(deliverer.store, delegator, pks[0]) + assert.Nil(t, bond, "expected delegatee bond %d to be nil", bond) + } +} + +func TestVoidCandidacy(t *testing.T) { + accounts, accStore := initAccounts(2, 1000) // for accounts + sender, delegator := accounts[0], accounts[1] + deliverer := newDeliver(t, sender, accStore) + + // create the candidate + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + require.NoError(t, got, "expected no error on runTxDeclareCandidacy") + + // bond a delegator + txDelegate := newTxDelegate(10, pks[0]) + deliverer.sender = delegator + got = deliverer.delegate(txDelegate) + require.NoError(t, got, "expected ok, got %v", got) + + // unbond the candidates bond portion + txUndelegate := newTxUnbond("10", pks[0]) + deliverer.sender = sender + got = deliverer.unbond(txUndelegate) + require.NoError(t, got, "expected no error on runTxDeclareCandidacy") + + // test that this pubkey cannot yet be bonded too + deliverer.sender = delegator + got = deliverer.delegate(txDelegate) + assert.Error(t, got, "expected error, got %v", got) + + // test that the delegator can still withdraw their bonds + got = deliverer.unbond(txUndelegate) + require.NoError(t, got, "expected no error on runTxDeclareCandidacy") + + // verify that the pubkey can now be reused + got = deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(t, got, "expected ok, got %v", got) +} diff --git a/x/stake/tick.go b/x/stake/tick.go new file mode 100644 index 0000000000..42c995da44 --- /dev/null +++ b/x/stake/tick.go @@ -0,0 +1,74 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/tendermint/abci/types" + "github.com/tendermint/tmlibs/rational" +) + +// Tick - called at the end of every block +func Tick(ctx sdk.Context, store types.KVStore) (change []*abci.Validator, err error) { + + // retrieve params + params := loadParams(store) + gs := loadGlobalState(store) + height := ctx.BlockHeight() + + // Process Validator Provisions + // XXX right now just process every 5 blocks, in new SDK make hourly + if gs.InflationLastTime+5 <= height { + gs.InflationLastTime = height + processProvisions(store, gs, params) + } + + return UpdateValidatorSet(store, gs, params) +} + +var hrsPerYr = rational.New(8766) // as defined by a julian year of 365.25 days + +// process provisions for an hour period +func processProvisions(store types.KVStore, gs *GlobalState, params Params) { + + gs.Inflation = nextInflation(gs, params).Round(1000000000) + + // Because the validators hold a relative bonded share (`GlobalStakeShare`), when + // more bonded tokens are added proportionally to all validators the only term + // which needs to be updated is the `BondedPool`. So for each previsions cycle: + + provisions := gs.Inflation.Mul(rational.New(gs.TotalSupply)).Quo(hrsPerYr).Evaluate() + gs.BondedPool += provisions + gs.TotalSupply += provisions + + // XXX XXX XXX XXX XXX XXX XXX XXX XXX + // XXX Mint them to the hold account + // XXX XXX XXX XXX XXX XXX XXX XXX XXX + + // save the params + saveGlobalState(store, gs) +} + +// get the next inflation rate for the hour +func nextInflation(gs *GlobalState, params Params) (inflation rational.Rat) { + + // The target annual inflation rate is recalculated for each previsions cycle. The + // inflation is also subject to a rate change (positive of negative) depending or + // the distance from the desired ratio (67%). The maximum rate change possible is + // defined to be 13% per year, however the annual inflation is capped as between + // 7% and 20%. + + // (1 - bondedRatio/GoalBonded) * InflationRateChange + inflationRateChangePerYear := rational.One.Sub(gs.bondedRatio().Quo(params.GoalBonded)).Mul(params.InflationRateChange) + inflationRateChange := inflationRateChangePerYear.Quo(hrsPerYr) + + // increase the new annual inflation for this next cycle + inflation = gs.Inflation.Add(inflationRateChange) + if inflation.GT(params.InflationMax) { + inflation = params.InflationMax + } + if inflation.LT(params.InflationMin) { + inflation = params.InflationMin + } + + return +} diff --git a/x/stake/tick_test.go b/x/stake/tick_test.go new file mode 100644 index 0000000000..add6be80b3 --- /dev/null +++ b/x/stake/tick_test.go @@ -0,0 +1,116 @@ +package stake + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/tmlibs/rational" +) + +func TestGetInflation(t *testing.T) { + store := initTestStore(t) + params := loadParams(store) + gs := loadGlobalState(store) + + // Governing Mechanism: + // bondedRatio = BondedPool / TotalSupply + // inflationRateChangePerYear = (1- bondedRatio/ GoalBonded) * MaxInflationRateChange + + tests := []struct { + setBondedPool, setTotalSupply int64 + setInflation, expectedChange rational.Rat + }{ + // with 0% bonded atom supply the inflation should increase by InflationRateChange + {0, 0, rational.New(7, 100), params.InflationRateChange.Quo(hrsPerYr)}, + + // 100% bonded, starting at 20% inflation and being reduced + {1, 1, rational.New(20, 100), rational.One.Sub(rational.One.Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, + + // 50% bonded, starting at 10% inflation and being increased + {1, 2, rational.New(10, 100), rational.One.Sub(rational.New(1, 2).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, + + // test 7% minimum stop (testing with 100% bonded) + {1, 1, rational.New(7, 100), rational.Zero}, + {1, 1, rational.New(70001, 1000000), rational.New(-1, 1000000)}, + + // test 20% maximum stop (testing with 0% bonded) + {0, 0, rational.New(20, 100), rational.Zero}, + {0, 0, rational.New(199999, 1000000), rational.New(1, 1000000)}, + + // perfect balance shouldn't change inflation + {67, 100, rational.New(15, 100), rational.Zero}, + } + for _, tc := range tests { + gs.BondedPool, gs.TotalSupply = tc.setBondedPool, tc.setTotalSupply + gs.Inflation = tc.setInflation + + inflation := nextInflation(gs, params) + diffInflation := inflation.Sub(tc.setInflation) + + assert.True(t, diffInflation.Equal(tc.expectedChange), + "%v, %v", diffInflation, tc.expectedChange) + } +} + +func TestProcessProvisions(t *testing.T) { + store := initTestStore(t) + params := loadParams(store) + gs := loadGlobalState(store) + + // create some candidates some bonded, some unbonded + n := 10 + actors := newActors(n) + candidates := candidatesFromActorsEmpty(actors) + for i, candidate := range candidates { + if i < 5 { + candidate.Status = Bonded + } + mintedTokens := int64((i + 1) * 10000000) + gs.TotalSupply += mintedTokens + candidate.addTokens(mintedTokens, gs) + saveCandidate(store, candidate) + } + var totalSupply int64 = 550000000 + var bondedShares int64 = 150000000 + var unbondedShares int64 = 400000000 + + // initial bonded ratio ~ 27% + assert.True(t, gs.bondedRatio().Equal(rational.New(bondedShares, totalSupply)), "%v", gs.bondedRatio()) + + // Supplies + assert.Equal(t, totalSupply, gs.TotalSupply) + assert.Equal(t, bondedShares, gs.BondedPool) + assert.Equal(t, unbondedShares, gs.UnbondedPool) + + // test the value of candidate shares + assert.True(t, gs.bondedShareExRate().Equal(rational.One), "%v", gs.bondedShareExRate()) + + initialSupply := gs.TotalSupply + initialUnbonded := gs.TotalSupply - gs.BondedPool + + // process the provisions a year + for hr := 0; hr < 8766; hr++ { + expInflation := nextInflation(gs, params).Round(1000000000) + expProvisions := (expInflation.Mul(rational.New(gs.TotalSupply)).Quo(hrsPerYr)).Evaluate() + startBondedPool := gs.BondedPool + startTotalSupply := gs.TotalSupply + processProvisions(store, gs, params) + assert.Equal(t, startBondedPool+expProvisions, gs.BondedPool) + assert.Equal(t, startTotalSupply+expProvisions, gs.TotalSupply) + } + assert.NotEqual(t, initialSupply, gs.TotalSupply) + assert.Equal(t, initialUnbonded, gs.UnbondedPool) + //panic(fmt.Sprintf("debug total %v, bonded %v, diff %v\n", gs.TotalSupply, gs.BondedPool, gs.TotalSupply-gs.BondedPool)) + + // initial bonded ratio ~ 35% ~ 30% increase for bonded holders + assert.True(t, gs.bondedRatio().Equal(rational.New(105906511, 305906511)), "%v", gs.bondedRatio()) + + // global supply + assert.Equal(t, int64(611813022), gs.TotalSupply) + assert.Equal(t, int64(211813022), gs.BondedPool) + assert.Equal(t, unbondedShares, gs.UnbondedPool) + + // test the value of candidate shares + assert.True(t, gs.bondedShareExRate().Mul(rational.New(bondedShares)).Equal(rational.New(211813022)), "%v", gs.bondedShareExRate()) + +}