cosmos-sdk/x/stake/handler.go

357 lines
8.8 KiB
Go

package stake
import (
"bytes"
"fmt"
"strconv"
crypto "github.com/tendermint/go-crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
)
// separated for testing
func InitState(ctx sdk.Context, mapper Mapper, key, value string) sdk.Error {
params := mapper.loadParams()
switch key {
case "allowed_bond_denom":
params.BondDenom = value
case "max_vals", "gas_bond", "gas_unbond":
i, err := strconv.Atoi(value)
if err != nil {
return sdk.ErrUnknownRequest(fmt.Sprintf("input must be integer, Error: %v", err.Error()))
}
switch key {
case "max_vals":
if i < 0 {
return sdk.ErrUnknownRequest("cannot designate negative max validators")
}
params.MaxVals = uint16(i)
case "gas_bond":
params.GasDelegate = int64(i)
case "gas_unbound":
params.GasUnbond = int64(i)
}
default:
return sdk.ErrUnknownRequest(key)
}
mapper.saveParams(params)
return nil
}
//_______________________________________________________________________
func NewHandler(mapper Mapper, ck bank.CoinKeeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
params := mapper.loadParams()
err := msg.ValidateBasic()
if err != nil {
return err.Result() // TODO should also return gasUsed?
}
signers := msg.GetSigners()
if len(signers) != 1 {
return sdk.ErrUnauthorized("there can only be one signer for staking transaction").Result()
}
sender := signers[0]
transact := newTransact(ctx, sender, mapper, ck)
// Run the transaction
switch msg := msg.(type) {
case MsgDeclareCandidacy:
res := transact.declareCandidacy(msg).Result()
if !ctx.IsCheckTx() {
res.GasUsed = params.GasDeclareCandidacy
}
return res
case MsgEditCandidacy:
res := transact.editCandidacy(msg).Result()
if !ctx.IsCheckTx() {
res.GasUsed = params.GasEditCandidacy
}
return res
case MsgDelegate:
res := transact.delegate(msg).Result()
if !ctx.IsCheckTx() {
res.GasUsed = params.GasDelegate
}
return res
case MsgUnbond:
res := transact.unbond(msg).Result()
if !ctx.IsCheckTx() {
res.GasUsed = params.GasUnbond
}
return res
default:
return sdk.ErrTxParse("invalid message parse in staking module").Result()
}
}
}
//_____________________________________________________________________
// common fields to all transactions
type transact struct {
ctx sdk.Context
sender crypto.Address
mapper Mapper
coinKeeper bank.CoinKeeper
params Params
gs *GlobalState
}
func newTransact(ctx sdk.Context, sender sdk.Address, mapper Mapper, ck bank.CoinKeeper) transact {
return transact{
ctx: ctx,
sender: sender,
mapper: mapper,
coinKeeper: ck,
params: mapper.loadParams(),
gs: mapper.loadGlobalState(),
}
}
//_____________________________________________________________________
// helper functions
// move a candidates asset pool from bonded to unbonded pool
func (tr transact) bondedToUnbondedPool(candidate *Candidate) {
// replace bonded shares with unbonded shares
tokens := tr.gs.removeSharesBonded(candidate.Assets)
candidate.Assets = tr.gs.addTokensUnbonded(tokens)
candidate.Status = Unbonded
}
// move a candidates asset pool from unbonded to bonded pool
func (tr transact) unbondedToBondedPool(candidate *Candidate) {
// replace unbonded shares with bonded shares
tokens := tr.gs.removeSharesUnbonded(candidate.Assets)
candidate.Assets = tr.gs.addTokensBonded(tokens)
candidate.Status = Bonded
}
// return an error if the bonds coins are incorrect
func checkDenom(mapper Mapper, bond sdk.Coin) sdk.Error {
if bond.Denom != mapper.loadParams().BondDenom {
return ErrBadBondingDenom()
}
return nil
}
//_____________________________________________________________________
// These functions assume everything has been authenticated,
// now we just perform action and save
func (tr transact) declareCandidacy(tx MsgDeclareCandidacy) sdk.Error {
// check to see if the pubkey or sender has been registered before
if tr.mapper.loadCandidate(tx.Address) != nil {
return ErrCandidateExistsAddr()
}
err := checkDenom(tr.mapper, tx.Bond)
if err != nil {
return err
}
if tr.ctx.IsCheckTx() {
return nil
}
candidate := NewCandidate(tx.PubKey, tr.sender, tx.Description)
tr.mapper.saveCandidate(candidate)
// move coins from the tr.sender account to a (self-bond) delegator account
// the candidate account and global shares are updated within here
txDelegate := NewMsgDelegate(tx.Address, tx.Bond)
return tr.delegateWithCandidate(txDelegate, candidate)
}
func (tr transact) editCandidacy(tx MsgEditCandidacy) sdk.Error {
// candidate must already be registered
if tr.mapper.loadCandidate(tx.Address) == nil {
return ErrBadCandidateAddr()
}
if tr.ctx.IsCheckTx() {
return nil
}
// Get the pubKey bond account
candidate := tr.mapper.loadCandidate(tx.Address)
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
}
tr.mapper.saveCandidate(candidate)
return nil
}
func (tr transact) delegate(tx MsgDelegate) sdk.Error {
if tr.mapper.loadCandidate(tx.Address) == nil {
return ErrBadCandidateAddr()
}
err := checkDenom(tr.mapper, tx.Bond)
if err != nil {
return err
}
if tr.ctx.IsCheckTx() {
return nil
}
// Get the pubKey bond account
candidate := tr.mapper.loadCandidate(tx.Address)
if candidate == nil {
return ErrBondNotNominated()
}
return tr.delegateWithCandidate(tx, candidate)
}
func (tr transact) delegateWithCandidate(tx MsgDelegate, candidate *Candidate) sdk.Error {
if candidate.Status == Revoked { //candidate has been withdrawn
return ErrBondNotNominated()
}
// Get or create the delegator bond
bond := tr.mapper.loadDelegatorBond(tr.sender, tx.Address)
if bond == nil {
bond = &DelegatorBond{
Address: tx.Address,
Shares: sdk.ZeroRat,
}
}
// Account new shares, save
err := bond.BondCoins(candidate, tx.Bond, tr)
if err != nil {
return err
}
tr.mapper.saveDelegatorBond(tr.sender, bond)
tr.mapper.saveCandidate(candidate)
tr.mapper.saveGlobalState(tr.gs)
return nil
}
func (tr transact) unbond(tx MsgUnbond) sdk.Error {
// check if bond has any shares in it unbond
bond := tr.mapper.loadDelegatorBond(tr.sender, tx.Address)
if bond == nil {
return ErrNoDelegatorForAddress()
}
if !bond.Shares.GT(sdk.ZeroRat) { // bond shares < tx shares
return ErrInsufficientFunds()
}
// if shares set to special case Max then we're good
if tx.Shares != "MAX" {
// test getting rational number from decimal provided
shares, err := sdk.NewRatFromDecimal(tx.Shares)
if err != nil {
return err
}
// test that there are enough shares to unbond
if !bond.Shares.GT(shares) {
return ErrNotEnoughBondShares(tx.Shares)
}
}
if tr.ctx.IsCheckTx() {
return nil
}
// retrieve the amount of bonds to remove (TODO remove redundancy already serialized)
var shares sdk.Rat
var err sdk.Error
if tx.Shares == "MAX" {
shares = bond.Shares
} else {
shares, err = sdk.NewRatFromDecimal(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 := tr.mapper.loadCandidate(tx.Address)
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 bytes.Equal(tr.sender, candidate.Address) &&
candidate.Status != Revoked {
revokeCandidacy = true
}
// remove the bond
tr.mapper.removeDelegatorBond(tr.sender, tx.Address)
} else {
tr.mapper.saveDelegatorBond(tr.sender, bond)
}
// Add the coins
returnAmount := candidate.removeShares(shares, tr.gs)
returnCoins := sdk.Coins{{tr.params.BondDenom, returnAmount}}
tr.coinKeeper.AddCoins(tr.ctx, tr.sender, returnCoins)
// lastly if an revoke candidate if necessary
if revokeCandidacy {
// change the share types to unbonded if they were not already
if candidate.Status == Bonded {
tr.bondedToUnbondedPool(candidate)
}
// lastly update the status
candidate.Status = Revoked
}
// deduct shares from the candidate and save
if candidate.Liabilities.IsZero() {
tr.mapper.removeCandidate(tx.Address)
} else {
tr.mapper.saveCandidate(candidate)
}
tr.mapper.saveGlobalState(tr.gs)
return nil
}