Skip to content

Commit

Permalink
feat: track delegation in lien wrapper account
Browse files Browse the repository at this point in the history
Enables us to compute the number of locked coins without
needing to fetch account state, which traverses the account's
staking data structures.
  • Loading branch information
JimLarson authored and michaelfig committed Mar 8, 2022
1 parent 059f26a commit 05da39f
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 172 deletions.
7 changes: 2 additions & 5 deletions golang/cosmos/proto/agoric/lien/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ syntax = "proto3";
package agoric.lien;

import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";
import "agoric/lien/lien.proto";

option go_package = "github.com/Agoric/agoric-sdk/golang/cosmos/x/lien/types";

Expand All @@ -21,8 +21,5 @@ message AccountLien {
string address = 1;

// The liened amount. Should be nonzero.
repeated cosmos.base.v1beta1.Coin lien = 2 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
Lien lien = 2;
}
12 changes: 11 additions & 1 deletion golang/cosmos/proto/agoric/lien/lien.proto
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,18 @@ option go_package = "github.com/Agoric/agoric-sdk/golang/cosmos/x/lien/types";

// Lien contains the lien state of a particular account.
message Lien {
// coins holds the amount liened
repeated cosmos.base.v1beta1.Coin coins = 1 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.moretags) = "yaml:\"coins\""
];
// delegated tracks the net amount delegated for non-vesting accounts,
// or zero coins for vesting accounts.
// (Vesting accounts have their own fields to track delegation.)
repeated cosmos.base.v1beta1.Coin delegated = 2 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.moretags) = "yaml:\"delegated\""
];
}
8 changes: 3 additions & 5 deletions golang/cosmos/x/lien/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,8 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, genesisState types.GenesisState
if err != nil {
panic(err) // not possible if genesis state was validated
}
lien := types.Lien{
Coins: accLien.GetLien(),
}
keeper.SetLien(ctx, addr, lien)
lien := accLien.GetLien()
keeper.SetLien(ctx, addr, *lien)
}
}

Expand All @@ -43,7 +41,7 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) types.GenesisState {
keeper.IterateLiens(ctx, func(addr sdk.AccAddress, lien types.Lien) bool {
accLien := types.AccountLien{
Address: addr.String(),
Lien: lien.GetCoins(),
Lien: &lien,
}
genesisState.Liens = append(genesisState.Liens, accLien)
return false
Expand Down
62 changes: 30 additions & 32 deletions golang/cosmos/x/lien/keeper/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,11 @@ func (la *LienAccount) LockedCoins(ctx sdk.Context) sdk.Coins {

// Returns the coins which are locked for lien encumbrance.
func (la *LienAccount) LienedLockedCoins(ctx sdk.Context) sdk.Coins {
state := la.lienKeeper.GetAccountState(ctx, la.GetAddress())
return computeLienLocked(state.Liened, state.Bonded, state.Unbonding)
}

// Returns the coins which are locked for lien encumbrance.
// The lien applies to bonded and unbonding coins before unbonded coins,
// so we return max(0, liened - (bonded + unbonding)).
func computeLienLocked(liened, bonded, unbonding sdk.Coins) sdk.Coins {
delegated := la.GetDelegatedFree().Add(la.GetDelegatedVesting()...)
liened := la.lien.Coins
// Since coins can't go negative, even transiently, use the
// identity A + B = max(A, B) + min(A, B)
// max(0, A - B) = max(B, A) - B = A - min(A, B)
delegated := bonded.Add(unbonding...)
return liened.Sub(liened.Min(delegated))
}

Expand All @@ -106,20 +99,27 @@ func NewAccountWrapper(lk Keeper) types.AccountWrapper {
if acc == nil {
return nil
}
if omni, ok := acc.(omniAccount); ok {
addr := acc.GetAddress()
lien := lk.GetLien(ctx, addr)
if lien.Coins.IsZero() {
// don't wrap unless there is a lien
return acc
}
return &LienAccount{
omniVestingAccount: makeVesting(omni),
lienKeeper: lk,
}
omni, ok := acc.(omniAccount)
if !ok {
// don't wrap non-omni accounts, e.g. module accounts
return acc
}
// don't wrap non-omni accounts, e.g. module accounts
return acc
addr := acc.GetAddress()
lien := lk.GetLien(ctx, addr)
if lien.Coins.IsZero() {
// don't wrap unless there is a lien
return acc
}
lienAcc := LienAccount{
lienKeeper: lk,
lien: lien,
}
vestingAcc, ok := omni.(omniVestingAccount)
if !ok {
vestingAcc = unlockedVestingAccount{omniAccount: omni, lien: &lienAcc.lien}
}
lienAcc.omniVestingAccount = vestingAcc
return &lienAcc
},
Unwrap: func(ctx sdk.Context, acc authtypes.AccountI) authtypes.AccountI {
if la, ok := acc.(*LienAccount); ok {
Expand All @@ -133,20 +133,12 @@ func NewAccountWrapper(lk Keeper) types.AccountWrapper {
}
}

// makeVesting returns a VestingAccount, wrapping a non-vesting argument
// in a trivial implementation if necessary.
func makeVesting(acc omniAccount) omniVestingAccount {
if v, ok := acc.(omniVestingAccount); ok {
return v
}
return unlockedVestingAccount{omniAccount: acc}
}

// unlockedVestingAccount extends an omniAccount to be a vesting account
// by simulating an original vesting amount of zero. It inherets the marshal
// behavior of the wrapped account.
type unlockedVestingAccount struct {
omniAccount
lien *types.Lien
}

var _ vestexported.VestingAccount = unlockedVestingAccount{}
Expand All @@ -157,9 +149,15 @@ func (uva unlockedVestingAccount) LockedCoins(ctx sdk.Context) sdk.Coins {
}

func (uva unlockedVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
if !amount.IsAllLTE(balance) {
panic("insufficient funds")
}
uva.lien.Delegated = uva.lien.Delegated.Add(amount...)
}

func (uva unlockedVestingAccount) TrackUndelegation(amount sdk.Coins) {
// max(delegated - amount, 0) == delegated - min(delegated, amount)
uva.lien.Delegated = uva.lien.Delegated.Sub(uva.lien.Delegated.Min(amount))
}

func (uva unlockedVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins {
Expand All @@ -183,7 +181,7 @@ func (uva unlockedVestingAccount) GetOriginalVesting() sdk.Coins {
}

func (uva unlockedVestingAccount) GetDelegatedFree() sdk.Coins {
return sdk.NewCoins()
return uva.lien.Delegated
}

func (uva unlockedVestingAccount) GetDelegatedVesting() sdk.Coins {
Expand Down
47 changes: 0 additions & 47 deletions golang/cosmos/x/lien/keeper/account_test.go

This file was deleted.

48 changes: 39 additions & 9 deletions golang/cosmos/x/lien/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Keeper interface {
GetLien(ctx sdk.Context, addr sdk.AccAddress) types.Lien
SetLien(ctx sdk.Context, addr sdk.AccAddress, lien types.Lien)
IterateLiens(ctx sdk.Context, cb func(addr sdk.AccAddress, lien types.Lien) bool)
UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newLien types.Lien) error
UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newCoin sdk.Coin) error
GetAccountState(ctx sdk.Context, addr sdk.AccAddress) types.AccountState
BondDenom(ctx sdk.Context) string
GetDelegatorDelegations(ctx sdk.Context, delegator sdk.AccAddress, maxRetrieve uint16) []stakingTypes.Delegation
Expand Down Expand Up @@ -102,19 +102,49 @@ func (lk keeperImpl) IterateLiens(ctx sdk.Context, cb func(addr sdk.AccAddress,
}
}

func (lk keeperImpl) UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newLien types.Lien) error {
// UpdateLien changes the liened amount of a single denomination in the given account.
// Either the old or new amount of the denomination can be zero.
// Liens can always be decreased, but to increase a lien, the new total amount must
// be vested and either bonded (for the staking token) or in the bank (for other tokens).
// The total lien can have several different denominations. Each is adjusted
// independently.
func (lk keeperImpl) UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newCoin sdk.Coin) error {
oldLien := lk.GetLien(ctx, addr)
if newLien.Coins.IsEqual(oldLien.Coins) {
oldCoins := oldLien.Coins
denom := newCoin.Denom
newAmount := newCoin.Amount
oldAmount := oldCoins.AmountOf(denom)
if newAmount.Equal(oldAmount) {
// no-op, no need to do anything
return nil
}
if !newLien.Coins.IsAllLTE(oldLien.Coins) {
// see if it's okay to increase the lien
state := lk.GetAccountState(ctx, addr)
if !newLien.Coins.IsAllLTE(state.Bonded) {
diff := newLien.Coins.Sub(newLien.Coins.Min(oldLien.Coins))
return fmt.Errorf("new lien higher than bonded amount by %s", diff)

newCoins := oldCoins.Sub(sdk.NewCoins(sdk.NewCoin(denom, oldAmount))).Add(newCoin)
newLien := types.Lien{
Coins: newCoins,
Delegated: oldLien.Delegated,
}

if newAmount.GT(oldAmount) {
// See if it's okay to increase the lien.
// Lien can be increased if the new amount is vested,
// not already liened, and if it's the bond denom,
// must be staked.
if denom == lk.BondDenom(ctx) {
state := lk.GetAccountState(ctx, addr)
bonded := state.Bonded.AmountOf(denom)
if !newAmount.LTE(bonded) {
return fmt.Errorf("want to lien %s but only %s bonded", newCoin, bonded)
}
newDelegated := bonded.Add(state.Unbonding.AmountOf(denom))
newLien.Delegated = sdk.NewCoins(sdk.NewCoin(denom, newDelegated))
} else {
inBank := lk.bankKeeper.GetBalance(ctx, addr, denom)
if !newAmount.LTE(inBank.Amount) {
return fmt.Errorf("want to lien %s but only %s available", newCoin, inBank)
}
}
// XXX check vested
}
lk.SetLien(ctx, addr, newLien)
return nil
Expand Down
2 changes: 1 addition & 1 deletion golang/cosmos/x/lien/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ func TestUpdateLien(t *testing.T) {
t.Fatalf("account state want %+v, got %+v", tt.state, gotState)
}
bondDenom := sk.BondDenom(ctx)
newLien := types.Lien{Coins: sdk.NewCoins(sdk.NewInt64Coin(bondDenom, tt.newLien))}
newLien := sdk.NewInt64Coin(bondDenom, tt.newLien)
err := lk.UpdateLien(ctx, addr2, newLien)
if err != nil {
if !tt.wantFail {
Expand Down
5 changes: 2 additions & 3 deletions golang/cosmos/x/lien/lien.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"math"

"github.com/Agoric/agoric-sdk/golang/cosmos/vm"
"github.com/Agoric/agoric-sdk/golang/cosmos/x/lien/types"

sdk "github.com/cosmos/cosmos-sdk/types"
)
Expand Down Expand Up @@ -190,9 +189,9 @@ func (ch portHandler) handleSetLiened(ctx sdk.Context, msg portMessage) (string,
if err = sdk.ValidateDenom(denom); err != nil {
return "", fmt.Errorf("invalid denom %s: %w", denom, err)
}
newLien := types.Lien{Coins: sdk.NewCoins(sdk.NewCoin(denom, msg.Amount))}
newCoin := sdk.NewCoin(denom, msg.Amount)

if err := ch.keeper.UpdateLien(ctx, addr, newLien); err != nil {
if err := ch.keeper.UpdateLien(ctx, addr, newCoin); err != nil {
return "false", nil
}
return "true", nil
Expand Down
22 changes: 10 additions & 12 deletions golang/cosmos/x/lien/lien_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,12 @@ var (
}
)

func ubld(amt int64) sdk.Coins {
return sdk.NewCoins(c("ubld", amt))
}

type mockLienKeeper struct {
states map[string]types.AccountState
validators map[string]stakingTypes.Validator
delegations map[string][]stakingTypes.Delegation
update *types.AccountLien
updateAddr sdk.AccAddress
updateCoin sdk.Coin
}

var _ Keeper = &mockLienKeeper{}
Expand All @@ -76,8 +73,9 @@ func (m *mockLienKeeper) SetLien(ctx sdk.Context, addr sdk.AccAddress, lien type
func (m *mockLienKeeper) IterateLiens(ctx sdk.Context, cb func(addr sdk.AccAddress, lien types.Lien) bool) {
}

func (m *mockLienKeeper) UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newLien types.Lien) error {
m.update = &types.AccountLien{Address: addr.String(), Lien: newLien.Coins}
func (m *mockLienKeeper) UpdateLien(ctx sdk.Context, addr sdk.AccAddress, newLien sdk.Coin) error {
m.updateAddr = addr
m.updateCoin = newLien
return nil
}

Expand Down Expand Up @@ -269,12 +267,12 @@ func TestSetLiened(t *testing.T) {
if reply != "true" {
t.Fatalf("Receive returned %s, want true", reply)
}
if keeper.update.Address != addr1 {
t.Errorf("lien update with address %s, want %s", keeper.update.Address, addr1)
if keeper.updateAddr.String() != addr1 {
t.Errorf("lien update with address %s, want %s", keeper.updateAddr, addr1)
}
wantLien := sdk.NewCoins(c("ubld", 123))
if !keeper.update.Lien.IsEqual(wantLien) {
t.Errorf("lien update got %s, want %s", keeper.update.Lien, wantLien)
wantCoin := c("ubld", 123)
if !keeper.updateCoin.IsEqual(wantCoin) {
t.Errorf("lien update got %s, want %s", keeper.updateCoin, wantCoin)
}
}

Expand Down
2 changes: 1 addition & 1 deletion golang/cosmos/x/lien/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

type BankKeeper interface {
GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins
//GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coins
GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin
}

type StakingKeeper interface {
Expand Down

0 comments on commit 05da39f

Please sign in to comment.