From 8266fb995cd48524139970ad2eefb08fc0789584 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Mon, 10 Jan 2022 16:48:48 -0600 Subject: [PATCH] safer payUser --- functions/src/resolve-market.ts | 130 +++----------------------------- functions/src/stripe.ts | 4 +- functions/src/utils.ts | 34 +++++++++ 3 files changed, 45 insertions(+), 123 deletions(-) diff --git a/functions/src/resolve-market.ts b/functions/src/resolve-market.ts index 215b2f73..8fdbe470 100644 --- a/functions/src/resolve-market.ts +++ b/functions/src/resolve-market.ts @@ -5,9 +5,13 @@ import * as _ from 'lodash' import { Contract } from '../../common/contract' import { User } from '../../common/user' import { Bet } from '../../common/bet' -import { CREATOR_FEE, PLATFORM_FEE } from '../../common/fees' -import { getUser } from './utils' +import { getUser, payUser } from './utils' import { sendMarketResolutionEmail } from './emails' +import { + getCancelPayouts, + getMktPayouts, + getStandardPayouts, +} from '../../common/payouts' export const resolveMarket = functions .runWith({ minInstances: 1 }) @@ -74,7 +78,9 @@ export const resolveMarket = functions _.sumBy(group, (g) => g.payout) ) - const payoutPromises = Object.entries(userPayouts).map(payUser) + const payoutPromises = Object.entries(userPayouts).map( + ([userId, payout]) => payUser(userId, payout) + ) const result = await Promise.all(payoutPromises) .catch((e) => ({ status: 'error', message: e })) @@ -115,121 +121,3 @@ const sendResolutionEmails = async ( } const firestore = admin.firestore() - -const getCancelPayouts = (truePool: number, bets: Bet[]) => { - console.log('resolved N/A, pool M$', truePool) - - const betSum = _.sumBy(bets, (b) => b.amount) - - return bets.map((bet) => ({ - userId: bet.userId, - payout: (bet.amount / betSum) * truePool, - })) -} - -const getStandardPayouts = ( - outcome: string, - truePool: number, - contract: Contract, - bets: Bet[] -) => { - const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - const winningBets = outcome === 'YES' ? yesBets : noBets - - const betSum = _.sumBy(winningBets, (b) => b.amount) - - if (betSum >= truePool) return getCancelPayouts(truePool, winningBets) - - const creatorPayout = CREATOR_FEE * truePool - console.log( - 'resolved', - outcome, - 'pool: M$', - truePool, - 'creator fee: M$', - creatorPayout - ) - - const shareDifferenceSum = _.sumBy(winningBets, (b) => b.shares - b.amount) - - const winningsPool = truePool - betSum - - const winnerPayouts = winningBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - (bet.amount + - ((bet.shares - bet.amount) / shareDifferenceSum) * winningsPool), - })) - - return winnerPayouts.concat([ - { userId: contract.creatorId, payout: creatorPayout }, - ]) // add creator fee -} - -const getMktPayouts = (truePool: number, contract: Contract, bets: Bet[]) => { - const p = - contract.pool.YES ** 2 / (contract.pool.YES ** 2 + contract.pool.NO ** 2) - console.log('Resolved MKT at p=', p, 'pool: $M', truePool) - - const [yesBets, noBets] = _.partition(bets, (bet) => bet.outcome === 'YES') - - const weightedBetTotal = - p * _.sumBy(yesBets, (b) => b.amount) + - (1 - p) * _.sumBy(noBets, (b) => b.amount) - - if (weightedBetTotal >= truePool) { - return bets.map((bet) => ({ - userId: bet.userId, - payout: - (((bet.outcome === 'YES' ? p : 1 - p) * bet.amount) / - weightedBetTotal) * - truePool, - })) - } - - const winningsPool = truePool - weightedBetTotal - - const weightedShareTotal = - p * _.sumBy(yesBets, (b) => b.shares - b.amount) + - (1 - p) * _.sumBy(noBets, (b) => b.shares - b.amount) - - const yesPayouts = yesBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - (p * bet.amount + - ((p * (bet.shares - bet.amount)) / weightedShareTotal) * winningsPool), - })) - - const noPayouts = noBets.map((bet) => ({ - userId: bet.userId, - payout: - (1 - fees) * - ((1 - p) * bet.amount + - (((1 - p) * (bet.shares - bet.amount)) / weightedShareTotal) * - winningsPool), - })) - - const creatorPayout = CREATOR_FEE * truePool - - return [ - ...yesPayouts, - ...noPayouts, - { userId: contract.creatorId, payout: creatorPayout }, - ] -} - -export const payUser = ([userId, payout]: [string, number]) => { - return firestore.runTransaction(async (transaction) => { - const userDoc = firestore.doc(`users/${userId}`) - const userSnap = await transaction.get(userDoc) - if (!userSnap.exists) return - const user = userSnap.data() as User - - const newUserBalance = user.balance + payout - transaction.update(userDoc, { balance: newUserBalance }) - }) -} - -const fees = PLATFORM_FEE + CREATOR_FEE diff --git a/functions/src/stripe.ts b/functions/src/stripe.ts index aac128cb..71ec4a71 100644 --- a/functions/src/stripe.ts +++ b/functions/src/stripe.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import Stripe from 'stripe' -import { payUser } from './resolve-market' +import { payUser } from './utils' const stripe = new Stripe(functions.config().stripe.apikey, { apiVersion: '2020-08-27', @@ -118,7 +118,7 @@ const issueMoneys = async (session: any) => { session, }) - await payUser([userId, payout]) + await payUser(userId, payout) console.log('user', userId, 'paid M$', payout) } diff --git a/functions/src/utils.ts b/functions/src/utils.ts index a2358243..887d602c 100644 --- a/functions/src/utils.ts +++ b/functions/src/utils.ts @@ -21,3 +21,37 @@ export const getContract = (contractId: string) => { export const getUser = (userId: string) => { return getValue('users', userId) } + +const firestore = admin.firestore() + +const updateUserBalance = (userId: string, delta: number) => { + return firestore.runTransaction(async (transaction) => { + const userDoc = firestore.doc(`users/${userId}`) + const userSnap = await transaction.get(userDoc) + if (!userSnap.exists) return + const user = userSnap.data() as User + + const newUserBalance = user.balance + delta + + if (newUserBalance < 0) + throw new Error( + `User (${userId}) balance cannot be negative: ${newUserBalance}` + ) + + transaction.update(userDoc, { balance: newUserBalance }) + }) +} + +export const payUser = (userId: string, payout: number) => { + if (!isFinite(payout) || payout <= 0) + throw new Error('Payout is not positive: ' + payout) + + return updateUserBalance(userId, payout) +} + +export const chargeUser = (userId: string, charge: number) => { + if (!isFinite(charge) || charge <= 0) + throw new Error('User charge is not positive: ' + charge) + + return updateUserBalance(userId, -charge) +}