Skip to content

Commit

Permalink
chore: add tests for banking
Browse files Browse the repository at this point in the history
Signed-off-by: Jeremy Letang <[email protected]>
  • Loading branch information
jeremyletang committed Oct 18, 2024
1 parent 5336c27 commit 636c385
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 15 deletions.
1 change: 1 addition & 0 deletions core/banking/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type Collateral interface {
Withdraw(ctx context.Context, party, asset string, amount *num.Uint) (*types.LedgerMovement, error)
EnableAsset(ctx context.Context, asset types.Asset) error
GetPartyGeneralAccount(party, asset string) (*types.Account, error)
GetPartyLockedForStaking(party, asset string) (*types.Account, error)
GetPartyVestedRewardAccount(partyID, asset string) (*types.Account, error)
TransferFunds(ctx context.Context,
transfers []*types.Transfer,
Expand Down
15 changes: 15 additions & 0 deletions core/banking/mocks/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

194 changes: 194 additions & 0 deletions core/banking/oneoff_transfers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestTransfers(t *testing.T) {
t.Run("onefoff not enough funds to transfer", testOneOffTransferNotEnoughFundsToTransfer)
t.Run("onefoff invalid transfers", testOneOffTransferInvalidTransfers)
t.Run("valid oneoff transfer", testValidOneOffTransfer)
t.Run("valid staking transfers", testStakingTransfers)
t.Run("valid oneoff with deliverOn", testValidOneOffTransferWithDeliverOn)
t.Run("valid oneoff with deliverOn in the past is done straight away", testValidOneOffTransferWithDeliverOnInThePastStraightAway)
t.Run("rejected if doesn't reach minimal amount", testRejectedIfDoesntReachMinimalAmount)
Expand Down Expand Up @@ -273,6 +274,199 @@ func testValidOneOffTransfer(t *testing.T) {
assert.NoError(t, e.TransferFunds(ctx, transfer))
}

func testStakingTransfers(t *testing.T) {
e := getTestEngine(t)

// let's do a massive fee, easy to test
e.OnTransferFeeFactorUpdate(context.Background(), num.NewDecimalFromFloat(1))
e.OnStakingAsset(context.Background(), "ETH")

ctx := context.Background()

t.Run("cannot transfer to another pubkey lock_for_staking", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeGeneral,
To: "10ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
ToAccountType: types.AccountTypeLockedForStaking,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.broker.EXPECT().Send(gomock.Any()).Times(1)
assert.EqualError(t, e.TransferFunds(ctx, transfer), "transfers to locked for staking allowed only from own general account")
})

t.Run("cannot transfer from lock_for_staking to another general account", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeLockedForStaking,
To: "10ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
ToAccountType: types.AccountTypeGeneral,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.broker.EXPECT().Send(gomock.Any()).Times(1)
assert.EqualError(t, e.TransferFunds(ctx, transfer), "transfers from locked for staking allowed only to own general account")
})

t.Run("can only transfer from lock_for_staking to own general account", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeLockedForStaking,
To: "0000000000000000000000000000000000000000000000000000000000000000",
ToAccountType: types.AccountTypeGlobalReward,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.broker.EXPECT().Send(gomock.Any()).Times(1)
assert.EqualError(t, e.TransferFunds(ctx, transfer), "can only transfer from locked for staking to general account")
})

t.Run("can transfer from general to locked_for_staking and emit stake deposited", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeGeneral,
To: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
ToAccountType: types.AccountTypeLockedForStaking,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

fromAcc := types.Account{
Balance: num.NewUint(100),
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.col.EXPECT().GetPartyGeneralAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)

// assert the calculation of fees and transfer request are correct
e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)

e.broker.EXPECT().Send(gomock.Any()).Times(4)

// expect a call to the stake accounting
e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
func(_ context.Context, evt *types.StakeLinking) {
assert.Equal(t, evt.Type, types.StakeLinkingTypeDeposited)
})
assert.NoError(t, e.TransferFunds(ctx, transfer))
})

t.Run("can transfer from locked_for_staking to general and emit stake removed", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeLockedForStaking,
To: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
ToAccountType: types.AccountTypeGeneral,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

fromAcc := types.Account{
Balance: num.NewUint(100),
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.col.EXPECT().GetPartyLockedForStaking(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)

// assert the calculation of fees and transfer request are correct
e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)

e.broker.EXPECT().Send(gomock.Any()).Times(4)

// expect a call to the stake accounting
e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
func(_ context.Context, evt *types.StakeLinking) {
assert.Equal(t, evt.Type, types.StakeLinkingTypeRemoved)
})
assert.NoError(t, e.TransferFunds(ctx, transfer))
})

t.Run("can transfer from vested to general and emit stake removed", func(t *testing.T) {
transfer := &types.TransferFunds{
Kind: types.TransferCommandKindOneOff,
OneOff: &types.OneOffTransfer{
TransferBase: &types.TransferBase{
From: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
FromAccountType: types.AccountTypeVestedRewards,
To: "03ae90688632c649c4beab6040ff5bd04dbde8efbf737d8673bbda792a110301",
ToAccountType: types.AccountTypeGeneral,
Asset: assetNameETH,
Amount: num.NewUint(10),
Reference: "someref",
},
},
}

fromAcc := types.Account{
Balance: num.NewUint(100),
}

// asset exists
e.assets.EXPECT().Get(gomock.Any()).Times(1).Return(
assets.NewAsset(&mockAsset{name: assetNameETH, quantum: num.DecimalFromFloat(100)}), nil)
e.col.EXPECT().GetPartyVestedRewardAccount(gomock.Any(), gomock.Any()).Times(1).Return(&fromAcc, nil)

// assert the calculation of fees and transfer request are correct
e.col.EXPECT().TransferFunds(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)

e.broker.EXPECT().Send(gomock.Any()).Times(4)

// expect a call to the stake accounting
e.stakeAccounting.EXPECT().AddEvent(gomock.Any(), gomock.Any()).Times(1).Do(
func(_ context.Context, evt *types.StakeLinking) {
assert.Equal(t, evt.Type, types.StakeLinkingTypeRemoved)
})
assert.NoError(t, e.TransferFunds(ctx, transfer))
})
}

func testValidOneOffTransferWithDeliverOnInThePastStraightAway(t *testing.T) {
e := getTestEngine(t)

Expand Down
7 changes: 6 additions & 1 deletion core/banking/transfers_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ func (e *Engine) makeFeeTransferForFundsTransfer(
}

switch fromAccountType {
case types.AccountTypeGeneral, types.AccountTypeVestedRewards:
case types.AccountTypeGeneral, types.AccountTypeVestedRewards, types.AccountTypeLockedForStaking:
default:
e.log.Panic("from account not supported",
logging.String("account-type", fromAccountType.String()),
Expand Down Expand Up @@ -332,6 +332,11 @@ func (e *Engine) ensureEnoughFundsForTransfer(
if err != nil {
return err
}
case types.AccountTypeLockedForStaking:
account, err = e.col.GetPartyLockedForStaking(from, asset.ID)
if err != nil {
return err
}
case types.AccountTypeVestedRewards:
// sending from sub account to owners general account
if fromDerivedKey != nil {
Expand Down
6 changes: 6 additions & 0 deletions core/collateral/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -4091,6 +4091,12 @@ func (e *Engine) GetPartyGeneralAccount(partyID, asset string) (*types.Account,
return e.GetAccountByID(generalID)
}

// GetPartyLockedForStaking returns a general account given the partyID.
func (e *Engine) GetPartyLockedForStaking(partyID, asset string) (*types.Account, error) {
generalID := e.accountID(noMarket, partyID, asset, types.AccountTypeLockedForStaking)
return e.GetAccountByID(generalID)
}

// GetPartyBondAccount returns a general account given the partyID.
func (e *Engine) GetPartyBondAccount(market, partyID, asset string) (*types.Account, error) {
id := e.accountID(
Expand Down
43 changes: 29 additions & 14 deletions core/types/banking.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,21 @@ const (
)

var (
ErrMissingTransferKind = errors.New("missing transfer kind")
ErrCannotTransferZeroFunds = errors.New("cannot transfer zero funds")
ErrInvalidFromAccount = errors.New("invalid from account")
ErrInvalidFromDerivedKey = errors.New("invalid from derived key")
ErrInvalidToAccount = errors.New("invalid to account")
ErrUnsupportedFromAccountType = errors.New("unsupported from account type")
ErrUnsupportedToAccountType = errors.New("unsupported to account type")
ErrEndEpochIsZero = errors.New("end epoch is zero")
ErrStartEpochIsZero = errors.New("start epoch is zero")
ErrInvalidFactor = errors.New("invalid factor")
ErrStartEpochAfterEndEpoch = errors.New("start epoch after end epoch")
ErrInvalidToForRewardAccountType = errors.New("to party is invalid for reward account type")
ErrMissingTransferKind = errors.New("missing transfer kind")
ErrCannotTransferZeroFunds = errors.New("cannot transfer zero funds")
ErrInvalidFromAccount = errors.New("invalid from account")
ErrInvalidFromDerivedKey = errors.New("invalid from derived key")
ErrInvalidToAccount = errors.New("invalid to account")
ErrUnsupportedFromAccountType = errors.New("unsupported from account type")
ErrUnsupportedToAccountType = errors.New("unsupported to account type")
ErrEndEpochIsZero = errors.New("end epoch is zero")
ErrStartEpochIsZero = errors.New("start epoch is zero")
ErrInvalidFactor = errors.New("invalid factor")
ErrStartEpochAfterEndEpoch = errors.New("start epoch after end epoch")
ErrInvalidToForRewardAccountType = errors.New("to party is invalid for reward account type")
ErrTransferFromLockedForStakingAllowedOnlyToOwnGeneralAccount = errors.New("transfers from locked for staking allowed only to own general account")
ErrTransferToLockedForStakingAllowedOnlyFromOwnGeneralAccount = errors.New("transfers to locked for staking allowed only from own general account")
ErrCanOnlyTransferFromLockedForStakingToGeneralAccount = errors.New("can only transfer from locked for staking to general account")
)

type TransferCommandKind int
Expand Down Expand Up @@ -93,6 +96,18 @@ func (t *TransferBase) IsValid() error {
return ErrCannotTransferZeroFunds
}

if t.FromAccountType == AccountTypeLockedForStaking && t.ToAccountType != AccountTypeGeneral {
return ErrCanOnlyTransferFromLockedForStakingToGeneralAccount
}

if t.FromAccountType == AccountTypeGeneral && t.ToAccountType == AccountTypeLockedForStaking && t.From != t.To {
return ErrTransferToLockedForStakingAllowedOnlyFromOwnGeneralAccount
}

if t.ToAccountType == AccountTypeGeneral && t.FromAccountType == AccountTypeLockedForStaking && t.From != t.To {
return ErrTransferFromLockedForStakingAllowedOnlyToOwnGeneralAccount
}

// check for derived account transfer
if t.FromDerivedKey != nil {
if !vgcrypto.IsValidVegaPubKey(*t.FromDerivedKey) {
Expand All @@ -110,7 +125,7 @@ func (t *TransferBase) IsValid() error {

// check for any other transfers
switch t.FromAccountType {
case AccountTypeGeneral, AccountTypeVestedRewards /*, AccountTypeLockedForStaking*/ :
case AccountTypeGeneral, AccountTypeVestedRewards, AccountTypeLockedForStaking:
break
default:
return ErrUnsupportedFromAccountType
Expand All @@ -122,7 +137,7 @@ func (t *TransferBase) IsValid() error {
return ErrInvalidToForRewardAccountType
}
case AccountTypeGeneral, AccountTypeLPFeeReward, AccountTypeMakerReceivedFeeReward, AccountTypeMakerPaidFeeReward, AccountTypeMarketProposerReward,
AccountTypeAverageNotionalReward, AccountTypeRelativeReturnReward, AccountTypeValidatorRankingReward, AccountTypeReturnVolatilityReward, AccountTypeRealisedReturnReward, AccountTypeEligibleEntitiesReward, AccountTypeBuyBackFees: /*, AccountTypeLockedForStaking*/
AccountTypeAverageNotionalReward, AccountTypeRelativeReturnReward, AccountTypeValidatorRankingReward, AccountTypeReturnVolatilityReward, AccountTypeRealisedReturnReward, AccountTypeEligibleEntitiesReward, AccountTypeBuyBackFees, AccountTypeLockedForStaking:
break
default:
return ErrUnsupportedToAccountType
Expand Down

0 comments on commit 636c385

Please sign in to comment.