Skip to content

Commit

Permalink
feat: automated purchase auctions
Browse files Browse the repository at this point in the history
  • Loading branch information
ze97286 committed Sep 16, 2024
1 parent 913fb42 commit 6aaa6fe
Show file tree
Hide file tree
Showing 57 changed files with 10,564 additions and 7,084 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

- [11644](https://github.com/vegaprotocol/vega/issues/11644) - `liveOnly` flag has been added to the `AMM` API to show only active `AMMs`.
- [11519](https://github.com/vegaprotocol/vega/issues/11519) - Add fees to position API types.

- [11685](https://github.com/vegaprotocol/vega/issues/11685) - Automated purchase support added.

### 🐛 Fixes

- [11672](https://github.com/vegaprotocol/vega/issues/11672) - Add missing fees in GraphQL bindings.
Expand Down
116 changes: 116 additions & 0 deletions commands/proposal_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ func checkProposalChanges(terms *protoTypes.ProposalTerms) Errors {
errs.Merge(checkVolumeDiscountProgram(terms, c))
case *protoTypes.ProposalTerms_UpdateVolumeRebateProgram:
errs.Merge(checkVolumeRebateProgram(terms, c))
case *protoTypes.ProposalTerms_NewAutomatedPurchase:
errs.Merge(checkAutomatedPurchaseConfig(c))
default:
return errs.FinalAddForProperty("proposal_submission.terms.change", ErrIsNotValid)
}
Expand Down Expand Up @@ -411,6 +413,120 @@ func checkReferralProgramChanges(changes *vegapb.ReferralProgramChanges, enactme
return errs
}

func checkAutomatedPurchaseConfig(newAutoPurchase *vegapb.ProposalTerms_NewAutomatedPurchase) Errors {
errs := NewErrors()
if newAutoPurchase.NewAutomatedPurchase == nil {
return errs.FinalAddForProperty("proposal_submission.terms.change.automated_purchase_config", ErrIsRequired)
}
if newAutoPurchase.NewAutomatedPurchase.Changes == nil {
return errs.FinalAddForProperty("proposal_submission.terms.change.automated_purchase_config.changes", ErrIsRequired)
}

change := newAutoPurchase.NewAutomatedPurchase.Changes
if len(change.From) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.from", ErrIsRequired)
}

// TODO @charlie check which account types are valid for this
if change.FromAccountType == protoTypes.AccountType_ACCOUNT_TYPE_UNSPECIFIED {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.from_account_type", ErrIsRequired)
}
if change.FromAccountType != protoTypes.AccountType_ACCOUNT_TYPE_BUY_BACK_FEES {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.from_account_type", ErrIsNotValid)
}
if change.ToAccountType == protoTypes.AccountType_ACCOUNT_TYPE_UNSPECIFIED {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.to_account_type", ErrIsRequired)
}
if change.ToAccountType != protoTypes.AccountType_ACCOUNT_TYPE_BUY_BACK_FEES {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.to_account_type", ErrIsNotValid)
}
if len(change.MarketId) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.market_id", ErrIsRequired)
}
if change.PriceOracle == nil {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.price_oracle", ErrIsRequired)
} else {
errs.Merge(checkDataSourceSpec(change.PriceOracle, "data_spec_for_price_oracle", "proposal_submission.terms.change.automated_purchase_config", false))
}
if len(change.OracleOffsetFactor) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.oracle_offset_factor", ErrIsRequired)
} else {
d, err := num.DecimalFromString(change.OracleOffsetFactor)
if err != nil {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.oracle_offset_factor", ErrNotAValidFloat)
} else if !d.IsPositive() {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.oracle_offset_factor", ErrMustBePositive)
}
}
if change.AuctionSchedule == nil {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_schedule", ErrIsRequired)
} else {
switch tp := change.AuctionSchedule.SourceType.(type) {
case *vegapb.DataSourceDefinition_External:
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_schedule", fmt.Errorf("auction schedule must be an internal time trigger"))
case *vegapb.DataSourceDefinition_Internal:
switch tp.Internal.SourceType.(type) {
case *vegapb.DataSourceDefinitionInternal_Time:
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_schedule", fmt.Errorf("auction schedule must be an internal time trigger"))
default:
}
}
errs.Merge(checkDataSourceSpec(change.AuctionSchedule, "data_spec_for_auction_schedule", "proposal_submission.terms.change.automated_purchase_config", false))
}
if len(change.AuctionDuration) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_duration", ErrIsRequired)
} else {
if _, err := time.ParseDuration(change.AuctionDuration); err != nil {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_duration", fmt.Errorf("must be a valid duration"))
}
}

if change.AuctionVolumeSnapshotSchedule == nil {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_volume_snapshot_schedule", ErrIsRequired)
} else {
switch tp := change.AuctionVolumeSnapshotSchedule.SourceType.(type) {
case *vegapb.DataSourceDefinition_External:
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_volume_snapshot_schedule", fmt.Errorf("auction snapshot volume schedule must be an internal time trigger"))
case *vegapb.DataSourceDefinition_Internal:
switch tp.Internal.SourceType.(type) {
case *vegapb.DataSourceDefinitionInternal_Time:
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.auction_volume_snapshot_schedule", fmt.Errorf("auction snapshot volume schedule must be an internal time trigger"))
default:
}
}
errs.Merge(checkDataSourceSpec(change.AuctionSchedule, "data_spec_for_auction_volume_snapshot_schedule", "proposal_submission.terms.change.automated_purchase_config", false))
}

var min, max *num.Uint
if len(change.MinimumAuctionSize) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.minimum_auction_size", ErrIsRequired)
} else {
minSize, overflow := num.UintFromString(change.MinimumAuctionSize, 10)
if overflow || minSize.IsZero() || minSize.IsNegative() {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.minimum_auction_size", ErrMustBePositive)
} else {
min = minSize
}
}
if len(change.MaximumAuctionSize) == 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.maximum_auction_size", ErrIsRequired)
} else {
maxSize, overflow := num.UintFromString(change.MinimumAuctionSize, 10)
if overflow || maxSize.IsZero() || maxSize.IsNegative() {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.maximum_auction_size", ErrMustBePositive)
} else {
max = maxSize
}
}
if min.GT(max) {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.maximum_auction_size", fmt.Errorf("must be greater than or equal to minimum_auction_size"))
}
if change.ExpiryTimestamp < 0 {
errs.AddForProperty("proposal_submission.terms.change.automated_purchase_config.expiry_timestamp", ErrMustBePositiveOrZero)
}
return errs
}

func checkVolumeRebateProgram(terms *vegapb.ProposalTerms, change *vegapb.ProposalTerms_UpdateVolumeRebateProgram) Errors {
errs := NewErrors()
if change.UpdateVolumeRebateProgram == nil {
Expand Down
101 changes: 76 additions & 25 deletions core/collateral/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -4688,6 +4688,10 @@ func (e *Engine) GetNetworkTreasuryAccount(asset string) (*types.Account, error)
return e.GetAccountByID(e.accountID(noMarket, systemOwner, asset, types.AccountTypeNetworkTreasury))
}

func (e *Engine) GetOrCreateBuyBackFeesAccountID(ctx context.Context, asset string) string {
return e.getOrCreateBuyBackFeesAccount(ctx, asset).ID
}

func (e *Engine) getOrCreateBuyBackFeesAccount(ctx context.Context, asset string) *types.Account {
accID := e.accountID(noMarket, systemOwner, asset, types.AccountTypeBuyBackFees)
acc, err := e.GetAccountByID(accID)
Expand Down Expand Up @@ -4728,6 +4732,26 @@ func (e *Engine) GetOrCreateNetworkTreasuryAccount(ctx context.Context, asset st
return ntAcc
}

func (e *Engine) getOrCreateNetworkAccount(ctx context.Context, asset string, accountType types.AccountType) *types.Account {
accID := e.accountID(noMarket, systemOwner, asset, accountType)
acc, err := e.GetAccountByID(accID)
if err == nil {
return acc
}
ntAcc := &types.Account{
ID: accID,
Asset: asset,
Owner: systemOwner,
Balance: num.UintZero(),
MarketID: noMarket,
Type: accountType,
}
e.accs[accID] = ntAcc
e.addAccountToHashableSlice(ntAcc)
e.broker.Send(events.NewAccountEvent(ctx, *ntAcc))
return ntAcc
}

func (e *Engine) GetGlobalInsuranceAccount(asset string) (*types.Account, error) {
return e.GetAccountByID(e.accountID(noMarket, systemOwner, asset, types.AccountTypeGlobalInsurance))
}
Expand Down Expand Up @@ -4801,19 +4825,23 @@ func (e *Engine) GetSystemAccountBalance(asset, market string, accountType types
return account.Balance.Clone(), nil
}

// TransferToHoldingAccount locks funds from general account into holding account account of the party.
func (e *Engine) TransferToHoldingAccount(ctx context.Context, transfer *types.Transfer) (*types.LedgerMovement, error) {
generalAccountID := e.accountID(transfer.Market, transfer.Owner, transfer.Amount.Asset, types.AccountTypeGeneral)
generalAccount, err := e.GetAccountByID(generalAccountID)
// TransferToHoldingAccount locks funds from accountTypeFrom into holding account account of the party.
func (e *Engine) TransferToHoldingAccount(ctx context.Context, transfer *types.Transfer, accountTypeFrom types.AccountType) (*types.LedgerMovement, error) {
party := transfer.Owner
if party == types.NetworkParty {
party = systemOwner
}
sourceAccountID := e.accountID(transfer.Market, party, transfer.Amount.Asset, accountTypeFrom)
sourceAccount, err := e.GetAccountByID(sourceAccountID)
if err != nil {
return nil, err
}

holdingAccountID := e.accountID(noMarket, transfer.Owner, transfer.Amount.Asset, types.AccountTypeHolding)
holdingAccountID := e.accountID(noMarket, party, transfer.Amount.Asset, types.AccountTypeHolding)
holdingAccount, err := e.GetAccountByID(holdingAccountID)
if err != nil {
// if the holding account doesn't exist yet we create it here
holdingAccountID, err := e.CreatePartyHoldingAccount(ctx, transfer.Owner, transfer.Amount.Asset)
holdingAccountID, err := e.CreatePartyHoldingAccount(ctx, party, transfer.Amount.Asset)
if err != nil {
return nil, err
}
Expand All @@ -4825,7 +4853,7 @@ func (e *Engine) TransferToHoldingAccount(ctx context.Context, transfer *types.T
MinAmount: transfer.Amount.Amount.Clone(),
Asset: transfer.Amount.Asset,
Type: types.TransferTypeHoldingAccount,
FromAccount: []*types.Account{generalAccount},
FromAccount: []*types.Account{sourceAccount},
ToAccount: []*types.Account{holdingAccount},
}

Expand All @@ -4845,15 +4873,19 @@ func (e *Engine) TransferToHoldingAccount(ctx context.Context, transfer *types.T
return res, nil
}

// ReleaseFromHoldingAccount releases locked funds from holding account back to the general account of the party.
func (e *Engine) ReleaseFromHoldingAccount(ctx context.Context, transfer *types.Transfer) (*types.LedgerMovement, error) {
holdingAccountID := e.accountID(noMarket, transfer.Owner, transfer.Amount.Asset, types.AccountTypeHolding)
// ReleaseFromHoldingAccount releases locked funds from holding account back to the toAccount of the party.
func (e *Engine) ReleaseFromHoldingAccount(ctx context.Context, transfer *types.Transfer, toAccountType types.AccountType) (*types.LedgerMovement, error) {
party := transfer.Owner
if party == types.NetworkParty {
party = systemOwner
}
holdingAccountID := e.accountID(noMarket, party, transfer.Amount.Asset, types.AccountTypeHolding)
holdingAccount, err := e.GetAccountByID(holdingAccountID)
if err != nil {
return nil, err
}

generalAccount, err := e.GetAccountByID(e.accountID(noMarket, transfer.Owner, transfer.Amount.Asset, types.AccountTypeGeneral))
targetAccount, err := e.GetAccountByID(e.accountID(noMarket, party, transfer.Amount.Asset, toAccountType))
if err != nil {
return nil, err
}
Expand All @@ -4864,7 +4896,7 @@ func (e *Engine) ReleaseFromHoldingAccount(ctx context.Context, transfer *types.
Asset: transfer.Amount.Asset,
Type: types.TransferTypeReleaseHoldingAccount,
FromAccount: []*types.Account{holdingAccount},
ToAccount: []*types.Account{generalAccount},
ToAccount: []*types.Account{targetAccount},
}

res, err := e.getLedgerEntries(ctx, req)
Expand Down Expand Up @@ -5018,9 +5050,14 @@ func (e *Engine) CreateSpotMarketAccounts(ctx context.Context, marketID, quoteAs
return err
}

// PartyHasSufficientBalance checks if the party has sufficient amount in the general account.
func (e *Engine) PartyHasSufficientBalance(asset, partyID string, amount *num.Uint) error {
acc, err := e.GetPartyGeneralAccount(partyID, asset)
// PartyHasSufficientBalance checks if the party has sufficient amount in the <fromAccountType> account.
func (e *Engine) PartyHasSufficientBalance(asset, partyID string, amount *num.Uint, fromAccountType types.AccountType) error {
party := partyID
if party == types.NetworkParty {
party = systemOwner
}
accId := e.accountID(noMarket, party, asset, fromAccountType)
acc, err := e.GetAccountByID(accId)
if err != nil {
return err
}
Expand Down Expand Up @@ -5056,28 +5093,42 @@ func (e *Engine) CreatePartyHoldingAccount(ctx context.Context, partyID, asset s
}

// TransferSpot transfers the given asset/quantity from partyID to partyID.
// The source partyID general account must exist in the asset, the target partyID general account in the asset is created if it doesn't yet exist.
func (e *Engine) TransferSpot(ctx context.Context, partyID, toPartyID, asset string, quantity *num.Uint) (*types.LedgerMovement, error) {
generalAccountID := e.accountID(noMarket, partyID, asset, types.AccountTypeGeneral)
generalAccount, err := e.GetAccountByID(generalAccountID)
// The source partyID fromAccountType account must exist in the asset, the target partyID account in the asset is created if it doesn't yet exist.
func (e *Engine) TransferSpot(ctx context.Context, party, toParty, asset string, quantity *num.Uint, fromAccountType types.AccountType, toAccountType types.AccountType) (*types.LedgerMovement, error) {
partyID := party
if party == types.NetworkParty {
partyID = systemOwner
}

toPartyID := toParty
if toParty == types.NetworkParty {
toPartyID = systemOwner
}

fromAccountID := e.accountID(noMarket, partyID, asset, fromAccountType)
fromAccount, err := e.GetAccountByID(fromAccountID)
if err != nil {
return nil, err
}

targetGeneralAccountID := e.accountID(noMarket, toPartyID, asset, types.AccountTypeGeneral)
toGeneralAccount, err := e.GetAccountByID(targetGeneralAccountID)
toAccountID := e.accountID(noMarket, toPartyID, asset, toAccountType)
toAccount, err := e.GetAccountByID(toAccountID)
if err != nil {
targetGeneralAccountID, _ = e.CreatePartyGeneralAccount(ctx, toPartyID, asset)
toGeneralAccount, _ = e.GetAccountByID(targetGeneralAccountID)
if toAccountType == types.AccountTypeGeneral {
toAccountID, _ = e.CreatePartyGeneralAccount(ctx, toPartyID, asset)
toAccount, _ = e.GetAccountByID(toAccountID)
} else if toPartyID == types.NetworkParty {
toAccount = e.getOrCreateNetworkAccount(ctx, asset, toAccountType)
}
}

req := &types.TransferRequest{
Amount: quantity.Clone(),
MinAmount: quantity.Clone(),
Asset: asset,
Type: types.TransferTypeSpot,
FromAccount: []*types.Account{generalAccount},
ToAccount: []*types.Account{toGeneralAccount},
FromAccount: []*types.Account{fromAccount},
ToAccount: []*types.Account{toAccount},
}

res, err := e.getLedgerEntries(ctx, req)
Expand Down
Loading

0 comments on commit 6aaa6fe

Please sign in to comment.