cosmos-sdk/x/accounts/defaults/lockup/lockup.go

674 lines
19 KiB
Go

package lockup
import (
"bytes"
"context"
"errors"
"time"
"github.com/cosmos/gogoproto/proto"
"cosmossdk.io/collections"
collcodec "cosmossdk.io/collections/codec"
"cosmossdk.io/core/address"
"cosmossdk.io/core/header"
errorsmod "cosmossdk.io/errors"
"cosmossdk.io/math"
"cosmossdk.io/x/accounts/accountstd"
lockuptypes "cosmossdk.io/x/accounts/defaults/lockup/types"
banktypes "cosmossdk.io/x/bank/types"
distrtypes "cosmossdk.io/x/distribution/types"
stakingtypes "cosmossdk.io/x/staking/types"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
var (
OriginalLockingPrefix = collections.NewPrefix(0)
DelegatedFreePrefix = collections.NewPrefix(1)
DelegatedLockingPrefix = collections.NewPrefix(2)
EndTimePrefix = collections.NewPrefix(3)
StartTimePrefix = collections.NewPrefix(4)
LockingPeriodsPrefix = collections.NewPrefix(5)
OwnerPrefix = collections.NewPrefix(6)
WithdrawedCoinsPrefix = collections.NewPrefix(7)
)
var (
CONTINUOUS_LOCKING_ACCOUNT = "continuous-locking-account"
DELAYED_LOCKING_ACCOUNT = "delayed-locking-account"
PERIODIC_LOCKING_ACCOUNT = "periodic-locking-account"
PERMANENT_LOCKING_ACCOUNT = "permanent-locking-account"
)
type getLockedCoinsFunc = func(ctx context.Context, time time.Time, denoms ...string) (sdk.Coins, error)
// newBaseLockup creates a new BaseLockup object.
func newBaseLockup(d accountstd.Dependencies) *BaseLockup {
BaseLockup := &BaseLockup{
Owner: collections.NewItem(d.SchemaBuilder, OwnerPrefix, "owner", collections.BytesValue),
OriginalLocking: collections.NewMap(d.SchemaBuilder, OriginalLockingPrefix, "original_locking", collections.StringKey, sdk.IntValue),
DelegatedFree: collections.NewMap(d.SchemaBuilder, DelegatedFreePrefix, "delegated_free", collections.StringKey, sdk.IntValue),
DelegatedLocking: collections.NewMap(d.SchemaBuilder, DelegatedLockingPrefix, "delegated_locking", collections.StringKey, sdk.IntValue),
WithdrawedCoins: collections.NewMap(d.SchemaBuilder, WithdrawedCoinsPrefix, "withdrawed_coins", collections.StringKey, sdk.IntValue),
addressCodec: d.AddressCodec,
headerService: d.Environment.HeaderService,
EndTime: collections.NewItem(d.SchemaBuilder, EndTimePrefix, "end_time", collcodec.KeyToValueCodec[time.Time](sdk.TimeKey)),
}
return BaseLockup
}
type BaseLockup struct {
// Owner is the address of the account owner.
Owner collections.Item[[]byte]
OriginalLocking collections.Map[string, math.Int]
DelegatedFree collections.Map[string, math.Int]
DelegatedLocking collections.Map[string, math.Int]
WithdrawedCoins collections.Map[string, math.Int]
addressCodec address.Codec
headerService header.Service
// lockup end time.
EndTime collections.Item[time.Time]
}
func (bva *BaseLockup) Init(ctx context.Context, msg *lockuptypes.MsgInitLockupAccount) (
*lockuptypes.MsgInitLockupAccountResponse, error,
) {
owner, err := bva.addressCodec.StringToBytes(msg.Owner)
if err != nil {
return nil, sdkerrors.ErrInvalidAddress.Wrapf("invalid 'owner' address: %s", err)
}
err = bva.Owner.Set(ctx, owner)
if err != nil {
return nil, err
}
funds := accountstd.Funds(ctx)
sortedAmt := funds.Sort()
for _, coin := range sortedAmt {
err = bva.OriginalLocking.Set(ctx, coin.Denom, coin.Amount)
if err != nil {
return nil, err
}
// Set initial value for all locked token
err = bva.WithdrawedCoins.Set(ctx, coin.Denom, math.ZeroInt())
if err != nil {
return nil, err
}
}
bondDenom, err := getStakingDenom(ctx)
if err != nil {
return nil, err
}
// Set initial value for all locked token
err = bva.DelegatedFree.Set(ctx, bondDenom, math.ZeroInt())
if err != nil {
return nil, err
}
// Set initial value for all locked token
err = bva.DelegatedLocking.Set(ctx, bondDenom, math.ZeroInt())
if err != nil {
return nil, err
}
err = bva.EndTime.Set(ctx, msg.EndTime)
if err != nil {
return nil, err
}
return &lockuptypes.MsgInitLockupAccountResponse{}, nil
}
func (bva *BaseLockup) Delegate(
ctx context.Context, msg *lockuptypes.MsgDelegate, getLockedCoinsFunc getLockedCoinsFunc,
) (
*lockuptypes.MsgExecuteMessagesResponse, error,
) {
err := bva.checkSender(ctx, msg.Sender)
if err != nil {
return nil, err
}
whoami := accountstd.Whoami(ctx)
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
if err != nil {
return nil, err
}
hs := bva.headerService.HeaderInfo(ctx)
balance, err := bva.getBalance(ctx, delegatorAddress, msg.Amount.Denom)
if err != nil {
return nil, err
}
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Amount.Denom)
if err != nil {
return nil, err
}
err = bva.TrackDelegation(
ctx,
sdk.Coins{*balance},
lockedCoins,
sdk.Coins{msg.Amount},
)
if err != nil {
return nil, err
}
msgDelegate := &stakingtypes.MsgDelegate{
DelegatorAddress: delegatorAddress,
ValidatorAddress: msg.ValidatorAddress,
Amount: msg.Amount,
}
resp, err := sendMessage(ctx, msgDelegate)
if err != nil {
return nil, err
}
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
}
func (bva *BaseLockup) Undelegate(
ctx context.Context, msg *lockuptypes.MsgUndelegate,
) (
*lockuptypes.MsgExecuteMessagesResponse, error,
) {
err := bva.checkSender(ctx, msg.Sender)
if err != nil {
return nil, err
}
whoami := accountstd.Whoami(ctx)
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
if err != nil {
return nil, err
}
err = bva.TrackUndelegation(ctx, sdk.Coins{msg.Amount})
if err != nil {
return nil, err
}
msgUndelegate := &stakingtypes.MsgUndelegate{
DelegatorAddress: delegatorAddress,
ValidatorAddress: msg.ValidatorAddress,
Amount: msg.Amount,
}
resp, err := sendMessage(ctx, msgUndelegate)
if err != nil {
return nil, err
}
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
}
func (bva *BaseLockup) WithdrawReward(
ctx context.Context, msg *lockuptypes.MsgWithdrawReward,
) (
*lockuptypes.MsgExecuteMessagesResponse, error,
) {
err := bva.checkSender(ctx, msg.Sender)
if err != nil {
return nil, err
}
whoami := accountstd.Whoami(ctx)
delegatorAddress, err := bva.addressCodec.BytesToString(whoami)
if err != nil {
return nil, err
}
msgWithdraw := &distrtypes.MsgWithdrawDelegatorReward{
DelegatorAddress: delegatorAddress,
ValidatorAddress: msg.ValidatorAddress,
}
responses, err := sendMessage(ctx, msgWithdraw)
if err != nil {
return nil, err
}
return &lockuptypes.MsgExecuteMessagesResponse{Responses: responses}, nil
}
func (bva *BaseLockup) SendCoins(
ctx context.Context, msg *lockuptypes.MsgSend, getLockedCoinsFunc getLockedCoinsFunc,
) (
*lockuptypes.MsgExecuteMessagesResponse, error,
) {
err := bva.checkSender(ctx, msg.Sender)
if err != nil {
return nil, err
}
whoami := accountstd.Whoami(ctx)
fromAddress, err := bva.addressCodec.BytesToString(whoami)
if err != nil {
return nil, err
}
hs := bva.headerService.HeaderInfo(ctx)
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Amount.Denoms()...)
if err != nil {
return nil, err
}
err = bva.checkTokensSendable(ctx, fromAddress, msg.Amount, lockedCoins)
if err != nil {
return nil, err
}
msgSend := &banktypes.MsgSend{
FromAddress: fromAddress,
ToAddress: msg.ToAddress,
Amount: msg.Amount,
}
resp, err := sendMessage(ctx, msgSend)
if err != nil {
return nil, err
}
return &lockuptypes.MsgExecuteMessagesResponse{Responses: resp}, nil
}
// WithdrawUnlockedCoins allow owner to withdraw the unlocked token for a specific denoms to an
// account of choice. Update the withdrawed token tracking for lockup account
func (bva *BaseLockup) WithdrawUnlockedCoins(
ctx context.Context, msg *lockuptypes.MsgWithdraw, getLockedCoinsFunc getLockedCoinsFunc,
) (
*lockuptypes.MsgWithdrawResponse, error,
) {
err := bva.checkSender(ctx, msg.Withdrawer)
if err != nil {
return nil, err
}
whoami := accountstd.Whoami(ctx)
fromAddress, err := bva.addressCodec.BytesToString(whoami)
if err != nil {
return nil, err
}
hs := bva.headerService.HeaderInfo(ctx)
lockedCoins, err := getLockedCoinsFunc(ctx, hs.Time, msg.Denoms...)
if err != nil {
return nil, err
}
amount := sdk.Coins{}
for _, denom := range msg.Denoms {
balance, err := bva.getBalance(ctx, fromAddress, denom)
if err != nil {
return nil, err
}
lockedAmt := lockedCoins.AmountOf(denom)
// get lockedCoin from that are not bonded for the sent denom
notBondedLockedCoin, err := bva.GetNotBondedLockedCoin(ctx, sdk.NewCoin(denom, lockedAmt), denom)
if err != nil {
return nil, err
}
spendable, err := balance.SafeSub(notBondedLockedCoin)
if err != nil {
return nil, errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds,
"locked amount exceeds account balance funds: %s > %s", notBondedLockedCoin, balance)
}
withdrawedAmt, err := bva.WithdrawedCoins.Get(ctx, denom)
if err != nil {
return nil, err
}
originalLockingAmt, err := bva.OriginalLocking.Get(ctx, denom)
if err != nil {
return nil, err
}
// withdrawable amount is equal to original locking amount subtract already withdrawed amount
withdrawableAmt, err := originalLockingAmt.SafeSub(withdrawedAmt)
if err != nil {
return nil, err
}
withdrawAmt := math.MinInt(withdrawableAmt, spendable.Amount)
// if zero amount go to the next iteration
if withdrawAmt.IsZero() {
continue
}
amount = append(amount, sdk.NewCoin(denom, withdrawAmt))
// update the withdrawed amount
err = bva.WithdrawedCoins.Set(ctx, denom, withdrawedAmt.Add(withdrawAmt))
if err != nil {
return nil, err
}
}
if len(amount) == 0 {
return nil, errors.New("no tokens available for withdrawing")
}
msgSend := &banktypes.MsgSend{
FromAddress: fromAddress,
ToAddress: msg.ToAddress,
Amount: amount,
}
_, err = sendMessage(ctx, msgSend)
if err != nil {
return nil, err
}
return &lockuptypes.MsgWithdrawResponse{
Receiver: msg.ToAddress,
AmountReceived: amount,
}, nil
}
func (bva *BaseLockup) checkSender(ctx context.Context, sender string) error {
owner, err := bva.Owner.Get(ctx)
if err != nil {
return sdkerrors.ErrInvalidAddress.Wrapf("invalid owner address: %s", err.Error())
}
senderBytes, err := bva.addressCodec.StringToBytes(sender)
if err != nil {
return sdkerrors.ErrInvalidAddress.Wrapf("invalid sender address: %s", err.Error())
}
if !bytes.Equal(owner, senderBytes) {
return errors.New("sender is not the owner of this vesting account")
}
return nil
}
func sendMessage(ctx context.Context, msg proto.Message) ([]*codectypes.Any, error) {
response, err := accountstd.ExecModuleUntyped(ctx, msg)
if err != nil {
return nil, err
}
respAny, err := accountstd.PackAny(response)
if err != nil {
return nil, err
}
return []*codectypes.Any{respAny}, nil
}
func getStakingDenom(ctx context.Context) (string, error) {
// Query account balance for the sent denom
paramsQueryReq := &stakingtypes.QueryParamsRequest{}
resp, err := accountstd.QueryModule[stakingtypes.QueryParamsResponse](ctx, paramsQueryReq)
if err != nil {
return "", err
}
return resp.Params.BondDenom, nil
}
// TrackDelegation tracks a delegation amount for any given lockup account type
// given the amount of coins currently being locked and the current account balance
// of the delegation denominations.
//
// CONTRACT: The account's coins, delegation coins, locked coins, and delegated
// locking coins must be sorted.
func (bva *BaseLockup) TrackDelegation(
ctx context.Context, balance, lockedCoins, amount sdk.Coins,
) error {
bondDenom, err := getStakingDenom(ctx)
if err != nil {
return err
}
delAmt := amount.AmountOf(bondDenom)
baseAmt := balance.AmountOf(bondDenom)
// return error if the delegation amount is zero or if the base coins does not
// exceed the desired delegation amount.
if delAmt.IsZero() || baseAmt.LT(delAmt) {
return sdkerrors.ErrInvalidCoins.Wrap("delegation attempt with zero coins for staking denom or insufficient funds")
}
lockedAmt := lockedCoins.AmountOf(bondDenom)
delLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
if err != nil {
return err
}
delFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
if err != nil {
return err
}
// compute x and y per the specification, where:
// X := min(max(V - DV, 0), D)
// Y := D - X
x := math.MinInt(math.MaxInt(lockedAmt.Sub(delLockingAmt), math.ZeroInt()), delAmt)
y := delAmt.Sub(x)
delLockingCoin := sdk.NewCoin(bondDenom, delLockingAmt)
delFreeCoin := sdk.NewCoin(bondDenom, delFreeAmt)
if !x.IsZero() {
xCoin := sdk.NewCoin(bondDenom, x)
newDelLocking := delLockingCoin.Add(xCoin)
err = bva.DelegatedLocking.Set(ctx, bondDenom, newDelLocking.Amount)
if err != nil {
return err
}
}
if !y.IsZero() {
yCoin := sdk.NewCoin(bondDenom, y)
newDelFree := delFreeCoin.Add(yCoin)
err = bva.DelegatedFree.Set(ctx, bondDenom, newDelFree.Amount)
if err != nil {
return err
}
}
return nil
}
// TrackUndelegation tracks an undelegation amount by setting the necessary
// values by which delegated locking and delegated free need to decrease and
// by which amount the base coins need to increase.
//
// NOTE: The undelegation (bond refund) amount may exceed the delegated
// locking (bond) amount due to the way undelegation truncates the bond refund,
// which can increase the validator's exchange rate (tokens/shares) slightly if
// the undelegated tokens are non-integral.
//
// CONTRACT: The account's coins and undelegation coins must be sorted.
func (bva *BaseLockup) TrackUndelegation(ctx context.Context, amount sdk.Coins) error {
bondDenom, err := getStakingDenom(ctx)
if err != nil {
return err
}
delAmt := amount.AmountOf(bondDenom)
// return error if the undelegation amount is zero
if delAmt.IsZero() {
return sdkerrors.ErrInvalidCoins.Wrap("undelegation attempt with zero coins for staking denom")
}
delFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
if err != nil {
return err
}
delLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
if err != nil {
return err
}
// compute x and y per the specification, where:
// X := min(DF, D)
// Y := min(DV, D - X)
x := math.MinInt(delFreeAmt, delAmt)
y := math.MinInt(delLockingAmt, delAmt.Sub(x))
delLockingCoin := sdk.NewCoin(bondDenom, delLockingAmt)
delFreeCoin := sdk.NewCoin(bondDenom, delFreeAmt)
if !x.IsZero() {
xCoin := sdk.NewCoin(bondDenom, x)
newDelFree := delFreeCoin.Sub(xCoin)
err = bva.DelegatedFree.Set(ctx, bondDenom, newDelFree.Amount)
if err != nil {
return err
}
}
if !y.IsZero() {
yCoin := sdk.NewCoin(bondDenom, y)
newDelLocking := delLockingCoin.Sub(yCoin)
err = bva.DelegatedLocking.Set(ctx, bondDenom, newDelLocking.Amount)
if err != nil {
return err
}
}
return nil
}
func (bva BaseLockup) getBalance(ctx context.Context, sender, denom string) (*sdk.Coin, error) {
// Query account balance for the sent denom
balanceQueryReq := &banktypes.QueryBalanceRequest{Address: sender, Denom: denom}
resp, err := accountstd.QueryModule[banktypes.QueryBalanceResponse](ctx, balanceQueryReq)
if err != nil {
return nil, err
}
return resp.Balance, nil
}
func (bva BaseLockup) checkTokensSendable(ctx context.Context, sender string, amount, lockedCoins sdk.Coins) error {
// Check if any sent tokens is exceeds lockup account balances
for _, coin := range amount {
balance, err := bva.getBalance(ctx, sender, coin.Denom)
if err != nil {
return err
}
lockedAmt := lockedCoins.AmountOf(coin.Denom)
// get lockedCoin from that are not bonded for the sent denom
notBondedLockedCoin, err := bva.GetNotBondedLockedCoin(ctx, sdk.NewCoin(coin.Denom, lockedAmt), coin.Denom)
if err != nil {
return err
}
spendable, hasNeg := sdk.Coins{*balance}.SafeSub(notBondedLockedCoin)
if hasNeg {
return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds,
"locked amount exceeds account balance funds: %s > %s", notBondedLockedCoin, balance)
}
if _, hasNeg := spendable.SafeSub(coin); hasNeg {
if len(spendable) == 0 {
spendable = sdk.Coins{sdk.NewCoin(coin.Denom, math.ZeroInt())}
}
return errorsmod.Wrapf(
sdkerrors.ErrInsufficientFunds,
"spendable balance %s is smaller than %s",
spendable, coin,
)
}
}
return nil
}
// IterateCoinEntries iterates over all the CoinEntries entries.
func (bva BaseLockup) IterateCoinEntries(
ctx context.Context,
entries collections.Map[string, math.Int],
cb func(denom string, value math.Int) (bool, error),
) error {
err := entries.Walk(ctx, nil, func(key string, value math.Int) (stop bool, err error) {
return cb(key, value)
})
return err
}
// GetNotBondedLockedCoin returns the coin that are not spendable that are not bonded by denom
// for a lockup account. If the coin by the provided denom are not locked, an coin with zero amount is returned.
func (bva BaseLockup) GetNotBondedLockedCoin(ctx context.Context, lockedCoin sdk.Coin, denom string) (sdk.Coin, error) {
bondDenom, err := getStakingDenom(ctx)
if err != nil {
return sdk.Coin{}, err
}
// if not bond denom then return the full locked coin
if bondDenom != denom {
return lockedCoin, nil
}
delegatedLockingAmt, err := bva.DelegatedLocking.Get(ctx, denom)
if err != nil {
return sdk.Coin{}, err
}
x := math.MinInt(lockedCoin.Amount, delegatedLockingAmt)
lockedAmt := lockedCoin.Amount.Sub(x)
return sdk.NewCoin(denom, lockedAmt), nil
}
// QueryLockupAccountBaseInfo returns a lockup account's info
func (bva BaseLockup) QueryLockupAccountBaseInfo(ctx context.Context, _ *lockuptypes.QueryLockupAccountInfoRequest) (
*lockuptypes.QueryLockupAccountInfoResponse, error,
) {
owner, err := bva.Owner.Get(ctx)
if err != nil {
return nil, err
}
ownerAddress, err := bva.addressCodec.BytesToString(owner)
if err != nil {
return nil, err
}
endTime, err := bva.EndTime.Get(ctx)
if err != nil {
return nil, err
}
originalLocking := sdk.Coins{}
err = bva.IterateCoinEntries(ctx, bva.OriginalLocking, func(key string, value math.Int) (stop bool, err error) {
originalLocking = append(originalLocking, sdk.NewCoin(key, value))
return false, nil
})
if err != nil {
return nil, err
}
bondDenom, err := getStakingDenom(ctx)
if err != nil {
return nil, err
}
delegatedLockingAmt, err := bva.DelegatedLocking.Get(ctx, bondDenom)
if err != nil {
return nil, err
}
delegatedLocking := sdk.NewCoins(sdk.NewCoin(bondDenom, delegatedLockingAmt))
delegatedFreeAmt, err := bva.DelegatedFree.Get(ctx, bondDenom)
if err != nil {
return nil, err
}
delegatedFree := sdk.NewCoins(sdk.NewCoin(bondDenom, delegatedFreeAmt))
return &lockuptypes.QueryLockupAccountInfoResponse{
Owner: ownerAddress,
OriginalLocking: originalLocking,
DelegatedLocking: delegatedLocking,
DelegatedFree: delegatedFree,
EndTime: &endTime,
}, nil
}
func (bva BaseLockup) RegisterExecuteHandlers(builder *accountstd.ExecuteBuilder) {
accountstd.RegisterExecuteHandler(builder, bva.Undelegate)
accountstd.RegisterExecuteHandler(builder, bva.WithdrawReward)
}