Keeper

The main logic of the Bet module definitions and methods.

Wagering

There are several steps for an incoming bet to be placed:

  1. Check for duplicate UID

  2. Validate the Ticket and unmarshal it to the wager payload type types.WagerTicketPayload

  3. KYC validation according to the creator address in the message and the KYC address of the ticket if required.

  4. Get the corresponding market from the market module and validate the odds existence.

  5. Validate the minimum bet amount according to the module parameters.

  6. Calculate and set the betting fee.

  7. Validate and calculate the payout profit according to the odds type, bet amount, and odds value.

  8. Call ProcessWager method of the Order Book module. this method handles the lock/unlock of the payout, participations, and bet fulfillments and transfers the payout and fees to the corresponding module accounts.

  9. Set the initial Status, Result,CreatedAt, and BetFulfillments attributes.

  10. Calculate the sequential ID, Set bet statistics, and bet to the bet module state.

// Wager stores a new bet in KVStore
func (k Keeper) Wager(ctx sdk.Context, bet *types.Bet) error {
	bettorAddress, err := sdk.AccAddressFromBech32(bet.Creator)
	if err != nil {
		return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "%s", err)
	}

	market, err := k.getMarket(ctx, bet.MarketUID)
	if err != nil {
		return err
	}

	// check if selected odds is valid
	if !market.HasOdds(bet.OddsUID) {
		return types.ErrOddsUIDNotExist
	}

	// check minimum bet amount allowed
	betConstraints := k.GetConstraints(ctx)

	if bet.Amount.LT(betConstraints.MinAmount) {
		return types.ErrBetAmountIsLow
	}

	// modify the bet fee and subtracted amount
	bet.SetFee(betConstraints.Fee)

	// calculate payoutProfit
	payoutProfit, err := types.CalculatePayoutProfit(bet.OddsType, bet.OddsValue, bet.Amount)
	if err != nil {
		return err
	}

	stats := k.GetBetStats(ctx)
	stats.Count++
	betID := stats.Count

	betFulfillment, err := k.orderbookKeeper.ProcessWager(
		ctx, bet.UID, bet.MarketUID, bet.OddsUID, bet.MaxLossMultiplier, bet.Amount, payoutProfit,
		bettorAddress, bet.Fee, bet.OddsType, bet.OddsValue, betID,
	)
	if err != nil {
		return sdkerrors.Wrapf(types.ErrInOBWagerProcessing, "%s", err)
	}

	// set bet as placed
	bet.Status = types.Bet_STATUS_PLACED

	// put bet in the result pending status
	bet.Result = types.Bet_RESULT_PENDING

	bet.CreatedAt = ctx.BlockTime().Unix()
	bet.BetFulfillment = betFulfillment

	// store bet in the module state
	k.SetBet(ctx, *bet, betID)

	// set bet as a pending bet
	k.SetPendingBet(ctx, types.NewPendingBet(bet.UID, bet.Creator), betID, bet.MarketUID)

	// set bet stats
	k.SetBetStats(ctx, stats)

	return nil
}

Settlement

There are several steps for an incoming bet to be settled:

  1. Check UUID validity

  2. Get the corresponding bet object

  3. Validates the bet creator stored in the state with the incoming bettorAddress of the settlement message.

  4. Check the bet status not to be canceled or settled before.

  5. Get the corresponding market from the market store and check if it exists, and is not aborted or canceled. If it is, calculate the payout and refund the bettor using RefundBettor method of the Order Book module keeper then sets the bet status as settled and finalizes the method.

  6. Sets the proper bet result according to the WinnerOddsUIDs.

  7. According to the bet result determined in the previous step, call BettorWins or BettorLoses of the Order Book module keeper.

  8. Set the settled bet in the bet module state.

// SettleBet settles a single bet and updates it in KVStore
func (k Keeper) SettleBet(ctx sdk.Context, bettorAddressStr, betUID string) error {
	if !utils.IsValidUID(betUID) {
		return types.ErrInvalidBetUID
	}

	uid2ID, found := k.GetBetID(ctx, betUID)
	if !found {
		return types.ErrNoMatchingBet
	}

	bet, found := k.GetBet(ctx, bettorAddressStr, uid2ID.ID)
	if !found {
		return types.ErrNoMatchingBet
	}

	bettorAddress, err := sdk.AccAddressFromBech32(bet.Creator)
	if err != nil {
		return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "%s", err)
	}

	if bet.Creator != bettorAddressStr {
		return types.ErrBettorAddressNotEqualToCreator
	}

	if err := bet.CheckSettlementEligiblity(); err != nil {
		// bet cancellation logic will reside here if this feature is requested
		return err
	}

	// get the respective market for the bet
	market, found := k.marketKeeper.GetMarket(ctx, bet.MarketUID)
	if !found {
		return types.ErrNoMatchingMarket
	}

	if market.Status == markettypes.MarketStatus_MARKET_STATUS_ABORTED ||
		market.Status == markettypes.MarketStatus_MARKET_STATUS_CANCELED {
		payoutProfit, err := types.CalculatePayoutProfit(bet.OddsType, bet.OddsValue, bet.Amount)
		if err != nil {
			return err
		}

		if err := k.orderbookKeeper.RefundBettor(ctx, bettorAddress, bet.Amount, bet.Fee, payoutProfit.TruncateInt(), bet.UID); err != nil {
			return sdkerrors.Wrapf(types.ErrInOBRefund, "%s", err)
		}

		bet.Status = types.Bet_STATUS_SETTLED
		bet.Result = types.Bet_RESULT_REFUNDED

		k.updateSettlementState(ctx, bet, uid2ID.ID)

		return nil
	}

	// check if the bet odds is a winner odds or not and set the bet pointer states
	if err := bet.SetResult(&market); err != nil {
		return err
	}

	if err := k.settleResolvedBet(ctx, &bet); err != nil {
		return err
	}

	if err := k.orderbookKeeper.WithdrawBetFee(ctx, sdk.MustAccAddressFromBech32(market.Creator), bet.Fee); err != nil {
		return err
	}

	k.updateSettlementState(ctx, bet, uid2ID.ID)

	return nil
}

// updateSettlementState settles bet in the store
func (k Keeper) updateSettlementState(ctx sdk.Context, bet types.Bet, betID uint64) {
	// set current height as settlement height
	bet.SettlementHeight = ctx.BlockHeight()

	// store bet in the module state
	k.SetBet(ctx, bet, betID)

	// remove pending bet
	k.RemovePendingBet(ctx, bet.MarketUID, betID)

	// store settled bet in the module state
	k.SetSettledBet(ctx, types.NewSettledBet(bet.UID, bet.Creator), betID, ctx.BlockHeight())
}

// settleResolvedBet settles a bet by calling order book functions to unlock fund and payout
// based on bet's result, and updates status of bet to settled
func (k Keeper) settleResolvedBet(ctx sdk.Context, bet *types.Bet) error {
	bettorAddress, err := sdk.AccAddressFromBech32(bet.Creator)
	if err != nil {
		return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "%s", err)
	}

	payout, err := types.CalculatePayoutProfit(bet.OddsType, bet.OddsValue, bet.Amount)
	if err != nil {
		return err
	}

	if bet.Result == types.Bet_RESULT_LOST {
		if err := k.orderbookKeeper.BettorLoses(ctx, bettorAddress, bet.Amount, payout.TruncateInt(), bet.UID, bet.BetFulfillment, bet.MarketUID); err != nil {
			return sdkerrors.Wrapf(types.ErrInOBBettorLoses, "%s", err)
		}
		bet.Status = types.Bet_STATUS_SETTLED
	} else if bet.Result == types.Bet_RESULT_WON {
		if err := k.orderbookKeeper.BettorWins(ctx, bettorAddress, bet.Amount, payout.TruncateInt(), bet.UID, bet.BetFulfillment, bet.MarketUID); err != nil {
			return sdkerrors.Wrapf(types.ErrInOBBettorWins, "%s", err)
		}
		bet.Status = types.Bet_STATUS_SETTLED
	}
	return nil
}

Batch Settlement

This is being used in the bet module end blocker in order to settle the bets automatically in batch.

// BatchMarketSettlements settles bets of resolved markets
// in batch. The markets get into account in FIFO manner.
func (k Keeper) BatchMarketSettlements(ctx sdk.Context) error {
	toFetch := k.GetParams(ctx).BatchSettlementCount

	// continue looping until reach batch settlement count parameter
	for toFetch > 0 {
		// get the first resolved market to process corresponding pending bets.
		marketUID, found := k.marketKeeper.GetFirstUnsettledResolvedMarket(ctx)
		// exit loop if there is no resolved bet.
		if !found {
			return nil
		}

		// settle market pending bets.
		settledCount, err := k.batchMarketSettlement(ctx, marketUID, toFetch)
		if err != nil {
			return fmt.Errorf("could not settle market %s %s", marketUID, err)
		}

		// check if still there is any pending bet for the market.
		pendingBetExists, err := k.IsAnyPendingBetForMarket(ctx, marketUID)
		if err != nil {
			return fmt.Errorf("could not check the pending bets %s %s", marketUID, err)
		}

		// if there is not any pending bet for the market
		// we need to remove its uid from the list of unsettled resolved bets.
		if !pendingBetExists {
			k.marketKeeper.RemoveUnsettledResolvedMarket(ctx, marketUID)
			err = k.orderbookKeeper.SetOrderBookAsUnsettledResolved(ctx, marketUID)
			if err != nil {
				return fmt.Errorf("could not resolve orderbook %s %s", marketUID, err)
			}
		}

		// update counter of bets to be processed in the next iteration.
		toFetch -= settledCount
	}

	return nil
}

// batchMarketSettlement settles pending bets of a markets
func (k Keeper) batchMarketSettlement(
	ctx sdk.Context,
	marketUID string,
	countToBeSettled uint32,
) (settledCount uint32, err error) {
	// initialize iterator for the certain number of pending bets
	// equal to countToBeSettled
	iterator := sdk.KVStorePrefixIteratorPaginated(
		ctx.KVStore(k.storeKey),
		types.PendingBetListOfMarketPrefix(marketUID),
		singlePageNum,
		uint(countToBeSettled))
	defer func() {
		iterErr := iterator.Close()
		if iterErr != nil {
			err = iterErr
		}
	}()

	// settle bets for the filtered pending bets
	for ; iterator.Valid(); iterator.Next() {
		var val types.PendingBet
		k.cdc.MustUnmarshal(iterator.Value(), &val)

		err = k.SettleBet(ctx, val.Creator, val.UID)
		if err != nil {
			return
		}

		// update total settled count
		settledCount++
	}

	return settledCount, nil
}

Last updated