├── .eslintignore ├── src ├── fpmm │ ├── price │ │ ├── index.ts │ │ ├── calcPrice.ts │ │ └── calcPrice.test.ts │ ├── trading │ │ ├── index.ts │ │ ├── calcBuyAmountInShares.test.ts │ │ ├── computeBalanceAfterSharePurchase.ts │ │ ├── computeBalanceAfterShareSale.ts │ │ ├── calcBuyAmountInShares.ts │ │ ├── computeBalanceAfterTrade.test.ts │ │ ├── computeBalanceAfterTrade.ts │ │ ├── calcSellAmountInCollateral.test.ts │ │ ├── computeBalanceAfterSharePurchase.test.ts │ │ ├── calcSellAmountInCollateral.ts │ │ └── computeBalanceAfterShareSale.test.ts │ ├── liquidity │ │ ├── calcPoolTokens.test.ts │ │ ├── index.ts │ │ ├── calcAddFundingSendAmounts.test.ts │ │ ├── calcDepositedTokens.test.ts │ │ ├── calcAddFundingSendAmounts.ts │ │ ├── calcInitialFundingSendAmounts.ts │ │ ├── calcInitialFundingSendAmounts.test.ts │ │ ├── calcAddFundingDepositedAmounts.test.ts │ │ ├── calcDepositedTokens.ts │ │ ├── calcPoolTokens.ts │ │ ├── calcInitialFundingDepositedAmounts.test.ts │ │ ├── calcRemoveFundingSendAmounts.ts │ │ ├── calcInitialFundingDepositedAmounts.ts │ │ ├── calcAddFundingDepositedAmounts.ts │ │ ├── calcDistributionHint.ts │ │ └── calcDistributionHint.test.ts │ └── index.ts ├── index.ts └── utils.ts ├── .gitignore ├── .commitlintrc.js ├── .lintstagedrc.js ├── tsconfig.production.json ├── .prettierrc ├── .npmignore ├── jest.config.js ├── tsconfig.json ├── .eslintrc.js ├── README.md └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # folders 2 | lib/ 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /src/fpmm/price/index.ts: -------------------------------------------------------------------------------- 1 | export { calcPrice } from "./calcPrice"; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # folders 2 | node_modules/ 3 | lib/ 4 | 5 | *.log 6 | *.tgz -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fpmm"; 2 | export { mulBN, divBN } from "./utils"; 3 | -------------------------------------------------------------------------------- /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.ts": ["eslint --fix"], 3 | "*.tsx": ["eslint --fix"], 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.production.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["src/**/*.test.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "printWidth": 120, 5 | "singleQuote": false, 6 | "tabWidth": 2, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #folders 2 | src 3 | test 4 | 5 | #files 6 | .commitlintrc.js 7 | .eslintignore 8 | .eslintrc.js 9 | .gitignore 10 | .lintstagedrc.js 11 | .prettierrc 12 | jest.config.js 13 | tsconfig.json 14 | tsconfig.production.json 15 | yarn.lock 16 | 17 | *.log 18 | *.tgz -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverageFrom: ["/src/**/*.ts", "!/src/**/*.d.ts"], 3 | coveragePathIgnorePatterns: ["node_modules"], 4 | coverageReporters: ["lcov", "html"], 5 | preset: "ts-jest", 6 | testEnvironment: "node", 7 | testMatch: ["**/src/**/*.test.ts"], 8 | }; -------------------------------------------------------------------------------- /src/fpmm/trading/index.ts: -------------------------------------------------------------------------------- 1 | export { computeBalanceAfterSharePurchase } from "./computeBalanceAfterSharePurchase"; 2 | export { computeBalanceAfterShareSale } from "./computeBalanceAfterShareSale"; 3 | export { calcBuyAmountInShares } from "./calcBuyAmountInShares"; 4 | export { calcSellAmountInCollateral } from "./calcSellAmountInCollateral"; 5 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcPoolTokens.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | 4 | import { calcPoolTokens } from "./calcPoolTokens"; 5 | 6 | describe("calcPoolTokens", () => { 7 | it("should return addedFunds if poolShares are zero", () => 8 | expect(calcPoolTokens(20, [1, 2, 3], 0)).toStrictEqual(BigNumber.from(20))); 9 | 10 | it("should return funds*supply/poolWeight", () => 11 | expect(calcPoolTokens(20, [1, 2, 3], 2)).toStrictEqual(BigNumber.from(13))); 12 | }); 13 | -------------------------------------------------------------------------------- /src/fpmm/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | calcPoolTokens, 3 | calcAddFundingSendAmounts, 4 | calcDepositedTokens, 5 | calcRemoveFundingSendAmounts, 6 | calcDistributionHint, 7 | calcInitialFundingSendAmounts, 8 | calcAddFundingDepositedAmounts, 9 | calcInitialFundingDepositedAmounts, 10 | } from "./liquidity"; 11 | export { calcPrice } from "./price"; 12 | export { 13 | computeBalanceAfterSharePurchase, 14 | computeBalanceAfterShareSale, 15 | calcBuyAmountInShares, 16 | calcSellAmountInCollateral, 17 | } from "./trading"; 18 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/index.ts: -------------------------------------------------------------------------------- 1 | export { calcAddFundingSendAmounts } from "./calcAddFundingSendAmounts"; 2 | export { calcDepositedTokens } from "./calcDepositedTokens"; 3 | export { calcDistributionHint } from "./calcDistributionHint"; 4 | export { calcInitialFundingSendAmounts } from "./calcInitialFundingSendAmounts"; 5 | export { calcPoolTokens } from "./calcPoolTokens"; 6 | export { calcRemoveFundingSendAmounts } from "./calcRemoveFundingSendAmounts"; 7 | export { calcAddFundingDepositedAmounts } from "./calcAddFundingDepositedAmounts"; 8 | export { calcInitialFundingDepositedAmounts } from "./calcInitialFundingDepositedAmounts"; 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "lib", 6 | "lib": ["dom"], 7 | "sourceMap": false, 8 | "allowJs": false, 9 | "declaration": true, 10 | "declarationDir": "lib", 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "allowSyntheticDefaultImports": true, 14 | "downlevelIteration": true, 15 | "esModuleInterop": true, 16 | "isolatedModules": true, 17 | "noImplicitAny": true, 18 | "skipLibCheck": true, 19 | "strict": true 20 | }, 21 | "include": ["src"], 22 | "exclude": ["node_modules", "lib"] 23 | } 24 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcAddFundingSendAmounts.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | import { Zero } from "@ethersproject/constants"; 4 | import { calcAddFundingSendAmounts } from "./calcAddFundingSendAmounts"; 5 | 6 | describe("calcAddFundingSendAmounts", () => { 7 | it("all holdings are different", () => { 8 | const result = calcAddFundingSendAmounts(10, [1, 2, 3]); 9 | 10 | expect(result).toStrictEqual([7, 4, 0].map(BigNumber.from)); 11 | }); 12 | 13 | it("all holdings are equal", () => { 14 | const result = calcAddFundingSendAmounts(10, [3, 3, 3]); 15 | 16 | expect(result).toStrictEqual([Zero, Zero, Zero]); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: "airbnb-base-typescript-prettier", 3 | parserOptions: { 4 | project: "./tsconfig.json", 5 | }, 6 | rules: { 7 | "@typescript-eslint/no-inferrable-types": "off", 8 | "@typescript-eslint/no-unused-vars": [ 9 | "error", 10 | { 11 | argsIgnorePattern: "_", 12 | varsIgnorePattern: "_", 13 | }, 14 | ], 15 | "import/extensions": [ 16 | "error", 17 | "ignorePackages", 18 | { 19 | ts: "never", 20 | }, 21 | ], 22 | "import/prefer-default-export": "off", 23 | "prefer-destructuring": "off", 24 | "prefer-template": "off", 25 | "@typescript-eslint/no-shadow": "off", 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcDepositedTokens.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | import { Zero } from "@ethersproject/constants"; 4 | import { calcDepositedTokens } from "./calcDepositedTokens"; 5 | 6 | describe("calcDepositedTokens", () => { 7 | it("returns min of holdings mapped to factor", () => 8 | expect(calcDepositedTokens(20, [1, 2, 3], 2)).toStrictEqual(BigNumber.from(10))); 9 | 10 | describe("when no holdings", () => { 11 | it("returns 0", () => expect(calcDepositedTokens(20, [100, 20, 0], 10)).toStrictEqual(Zero)); 12 | }); 13 | 14 | describe("when no funding", () => { 15 | it("returns 0", () => expect(calcDepositedTokens(20, [100, 200, 300], 0)).toStrictEqual(Zero)); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcAddFundingSendAmounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { calcAddFundingDepositedAmounts } from "./calcAddFundingDepositedAmounts"; 3 | 4 | /** 5 | * Compute the numbers of outcome tokens that will be sent to the user by the market maker after adding `addedFunds` of collateral. 6 | * @param addedFunds - the amount of collateral being added to the market maker as liquidity 7 | * @param poolBalances - the market maker's balances of outcome tokens 8 | */ 9 | export const calcAddFundingSendAmounts = (addedFunds: BigNumberish, poolBalances: BigNumberish[]): BigNumber[] => { 10 | const depositAmounts = calcAddFundingDepositedAmounts(addedFunds, poolBalances); 11 | 12 | const sendAmounts = depositAmounts.map(depositAmount => BigNumber.from(addedFunds).sub(depositAmount)); 13 | 14 | return sendAmounts; 15 | }; 16 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcInitialFundingSendAmounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { calcAddFundingSendAmounts } from "./calcAddFundingSendAmounts"; 3 | 4 | /** 5 | * Compute the number of outcome tokens that will be sent to the user by the market maker after funding it for the first time with `addedFunds` of collateral. 6 | * @dev The distribution hint plays the role of the pool's balances so we can just forward this to calcAddFundingSendAmounts 7 | * @param addedFunds - the amount of collateral being added to the market maker as liquidity 8 | * @param distributionHint - a distribution hint as calculated by `calcDistributionHint` 9 | */ 10 | export const calcInitialFundingSendAmounts = ( 11 | addedFunds: BigNumberish, 12 | distributionHint: BigNumberish[], 13 | ): BigNumber[] => calcAddFundingSendAmounts(addedFunds, distributionHint); 14 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcInitialFundingSendAmounts.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | import { Zero } from "@ethersproject/constants"; 4 | import { calcInitialFundingSendAmounts } from "./calcInitialFundingSendAmounts"; 5 | 6 | describe("calcInitialFundingSendAmounts", () => { 7 | it("all holdings are different", () => { 8 | const result = calcInitialFundingSendAmounts(10, [1, 2, 3]); 9 | 10 | expect(result).toStrictEqual(["7", "4", "0"].map(BigNumber.from)); 11 | }); 12 | 13 | it("all holdings are equal", () => { 14 | const result = calcInitialFundingSendAmounts(10, [3, 3, 3]); 15 | 16 | expect(result).toStrictEqual([Zero, Zero, Zero]); 17 | }); 18 | 19 | it("no funding", () => { 20 | const result = calcInitialFundingSendAmounts(0, [3, 3, 3]); 21 | expect(result).toStrictEqual([Zero, Zero, Zero]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/fpmm/price/calcPrice.ts: -------------------------------------------------------------------------------- 1 | import { BigNumberish } from "@ethersproject/bignumber"; 2 | import Big from "big.js"; 3 | 4 | /** 5 | * Computes the price of each outcome token given their holdings. Returns an array of numbers in the range [0, 1] 6 | * @param poolBalances - the market maker's balances of outcome tokens 7 | */ 8 | export const calcPrice = (poolBalances: BigNumberish[]): number[] => { 9 | const balances = poolBalances.map(h => new Big(h.toString())); 10 | 11 | const hasZeroBalances = balances.every(h => h.toString() === "0"); 12 | if (hasZeroBalances) { 13 | return balances.map(() => 0); 14 | } 15 | 16 | const product = balances.reduce((a, b) => a.mul(b)); 17 | const denominator = balances.map(h => product.div(h)).reduce((a, b) => a.add(b)); 18 | 19 | const prices = balances.map(holding => product.div(holding).div(denominator)); 20 | 21 | return prices.map(price => +price.valueOf()); 22 | }; 23 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcAddFundingDepositedAmounts.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | import { calcAddFundingDepositedAmounts } from "./calcAddFundingDepositedAmounts"; 4 | 5 | describe("calcAddFundingDepositedAmounts", () => { 6 | it("all holdings are different", () => { 7 | const result = calcAddFundingDepositedAmounts(10, [1, 2, 3]); 8 | 9 | expect(result).toStrictEqual([3, 6, 10].map(BigNumber.from)); 10 | }); 11 | 12 | it("all holdings are equal", () => { 13 | const result = calcAddFundingDepositedAmounts(10, [3, 3, 3]); 14 | 15 | expect(result).toStrictEqual([10, 10, 10].map(BigNumber.from)); 16 | }); 17 | 18 | it("initial pool balances includes a zero", () => { 19 | expect(() => calcAddFundingDepositedAmounts(10, [0, 0, 0])).toThrowError( 20 | "Invalid Pool Balances - you must provide a distribution hint for the desired weightings of the pool", 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcDepositedTokens.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { calcRemoveFundingSendAmounts } from "./calcRemoveFundingSendAmounts"; 3 | 4 | /** 5 | * Compute the amount of collateral that can be obtained via merging after the user 6 | * removed `removedFunds` of pool shares. 7 | * @param removedFunds - the amount of liquidity pool tokens being sent to the market maker in return for underlying outcome tokens 8 | * @param poolBalances - the market maker's balances of outcome tokens 9 | * @param poolShareSupply - the total supply of liquidity pool tokens 10 | */ 11 | export const calcDepositedTokens = ( 12 | removedFunds: BigNumberish, 13 | poolBalances: BigNumberish[], 14 | poolShareSupply: BigNumberish, 15 | ): BigNumber => { 16 | const sendAmounts = calcRemoveFundingSendAmounts(removedFunds, poolBalances, poolShareSupply); 17 | return sendAmounts.reduce((min, amount) => (amount.lt(min) ? amount : min)); 18 | }; 19 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcPoolTokens.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | 3 | /** 4 | * Compute the number of liquidity pool tokens that will be sent to the user by the Market Maker 5 | * after adding `addedFunds` of collateral. 6 | * @param addedFunds - the amount of collateral being added to the market maker as liquidity 7 | * @param poolBalances - the market maker's balances of outcome tokens 8 | * @param poolShareSupply - the total supply of liquidity pool tokens 9 | */ 10 | export const calcPoolTokens = ( 11 | addedFunds: BigNumberish, 12 | poolBalances: BigNumberish[], 13 | poolShareSupply: BigNumberish, 14 | ): BigNumber => { 15 | if (BigNumber.from(poolShareSupply).eq(0)) { 16 | return BigNumber.from(addedFunds); 17 | } 18 | 19 | const poolWeight = poolBalances.reduce((max, h) => (BigNumber.from(h).gt(max) ? h : max)); 20 | return BigNumber.from(addedFunds) 21 | .mul(poolShareSupply) 22 | .div(poolWeight); 23 | }; 24 | -------------------------------------------------------------------------------- /src/fpmm/price/calcPrice.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { calcPrice } from "./calcPrice"; 3 | 4 | const testCases: [number[], number[]][] = [ 5 | [ 6 | [100, 100], 7 | [0.5, 0.5], 8 | ], 9 | [ 10 | [150, 50], 11 | [0.25, 0.75], 12 | ], 13 | [ 14 | [50, 150], 15 | [0.75, 0.25], 16 | ], 17 | [ 18 | [100, 100, 100], 19 | [0.3333, 0.3333, 0.3333], 20 | ], 21 | [ 22 | [200, 100, 100], 23 | [0.2, 0.4, 0.4], 24 | ], 25 | [ 26 | [100, 200, 100], 27 | [0.4, 0.2, 0.4], 28 | ], 29 | [ 30 | [100, 100, 200], 31 | [0.4, 0.4, 0.2], 32 | ], 33 | [ 34 | [100, 200, 300], 35 | [0.5454, 0.2727, 0.1818], 36 | ], 37 | ]; 38 | 39 | describe("calcPrice", () => { 40 | it.each(testCases)(`should compute the right price`, (balances, expectedPrices) => { 41 | const prices = calcPrice(balances); 42 | 43 | prices.forEach((price, index) => expect(price).toBeCloseTo(expectedPrices[index])); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcInitialFundingDepositedAmounts.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | import { calcInitialFundingDepositedAmounts } from "./calcInitialFundingDepositedAmounts"; 4 | 5 | describe("calcInitialFundingDepositedAmounts", () => { 6 | it("Distribution hint is non-uniform", () => { 7 | const result = calcInitialFundingDepositedAmounts(10, [1, 2, 3]); 8 | 9 | expect(result).toStrictEqual([3, 6, 10].map(BigNumber.from)); 10 | }); 11 | 12 | it("Distribution hint is uniform", () => { 13 | const result = calcInitialFundingDepositedAmounts(10, [3, 3, 3]); 14 | 15 | expect(result).toStrictEqual([10, 10, 10].map(BigNumber.from)); 16 | }); 17 | 18 | it("Distribution hint includes a zero", () => { 19 | expect(() => calcInitialFundingDepositedAmounts(BigNumber.from(10), [0, 0, 0].map(BigNumber.from))).toThrowError( 20 | "Invalid Distribution Hint - can't assign a weight of zero to an outcome", 21 | ); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcRemoveFundingSendAmounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { Zero } from "@ethersproject/constants"; 3 | 4 | /** 5 | * Compute the number of outcome tokens that will be sent to the user by the Market Maker 6 | * after removing `removedFunds` of pool shares. 7 | * @param removedFunds - the amount of liquidity pool tokens being sent to the market maker in return for underlying outcome tokens 8 | * @param poolBalances - the market maker's balances of outcome tokens 9 | * @param poolShareSupply - the total supply of liquidity pool tokens 10 | */ 11 | export const calcRemoveFundingSendAmounts = ( 12 | removedFunds: BigNumberish, 13 | poolBalances: BigNumberish[], 14 | poolShareSupply: BigNumberish, 15 | ): BigNumber[] => { 16 | const sendAmounts = poolBalances.map(h => 17 | BigNumber.from(poolShareSupply).gt(0) 18 | ? BigNumber.from(h) 19 | .mul(removedFunds) 20 | .div(poolShareSupply) 21 | : Zero, 22 | ); 23 | return sendAmounts; 24 | }; 25 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcInitialFundingDepositedAmounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { calcAddFundingDepositedAmounts } from "./calcAddFundingDepositedAmounts"; 3 | 4 | /** 5 | * Compute the numbers of outcome tokens that will be added to the market maker after adding `addedFunds` of collateral. 6 | * @dev The distribution hint plays the role of the pool's balances so we can just forward this to calcAddFundingSendAmounts 7 | * @param addedFunds - the amount of collateral being added to the market maker as liquidity 8 | * @param poolBalances - the market maker's balances of outcome tokens 9 | */ 10 | export const calcInitialFundingDepositedAmounts = ( 11 | addedFunds: BigNumberish, 12 | distributionHint: BigNumberish[], 13 | ): BigNumber[] => { 14 | if (distributionHint.some(x => BigNumber.from(x).isZero())) { 15 | throw new Error("Invalid Distribution Hint - can't assign a weight of zero to an outcome"); 16 | } 17 | return calcAddFundingDepositedAmounts(addedFunds, distributionHint); 18 | }; 19 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcAddFundingDepositedAmounts.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | 3 | /** 4 | * Compute the numbers of outcome tokens that will be added to the market maker after adding `addedFunds` of collateral. 5 | * @param addedFunds - the amount of collateral being added to the market maker as liquidity 6 | * @param poolBalances - the market maker's balances of outcome tokens 7 | */ 8 | export const calcAddFundingDepositedAmounts = (addedFunds: BigNumberish, poolBalances: BigNumberish[]): BigNumber[] => { 9 | if (poolBalances.some(x => BigNumber.from(x).isZero())) { 10 | throw new Error( 11 | "Invalid Pool Balances - you must provide a distribution hint for the desired weightings of the pool", 12 | ); 13 | } 14 | 15 | const poolWeight = poolBalances.reduce((a, b) => (BigNumber.from(a).gt(b) ? a : b)); 16 | 17 | const depositAmounts = poolBalances.map(h => 18 | BigNumber.from(addedFunds) 19 | .mul(h) 20 | .div(poolWeight), 21 | ); 22 | 23 | return depositAmounts; 24 | }; 25 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcDistributionHint.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from "@ethersproject/bignumber"; 2 | import Big from "big.js"; 3 | 4 | /** 5 | * Computes the distribution hint that should be used for setting the initial odds to `initialOdds` 6 | * @param initialOdds - an array of numbers proportional to the initial estimate of the probability of each outcome 7 | */ 8 | export const calcDistributionHint = (initialOdds: number[]): BigNumber[] => { 9 | if (initialOdds.some(x => x === 0)) { 10 | throw new Error("Invalid probability - can't assign a probability of zero to an outcome"); 11 | } 12 | 13 | const allEqual = initialOdds.every(x => x === initialOdds[0]); 14 | if (allEqual) { 15 | return []; 16 | } 17 | 18 | const initialOddsBig = initialOdds.map(x => new Big(x)); 19 | const product = initialOddsBig.reduce((a, b) => a.mul(b)); 20 | 21 | const distributionHint = initialOddsBig 22 | .map(o => product.div(o)) 23 | .map(x => x.mul(1000000).round()) 24 | .map(x => BigNumber.from(x.toString())); 25 | 26 | return distributionHint; 27 | }; 28 | -------------------------------------------------------------------------------- /src/fpmm/trading/calcBuyAmountInShares.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { Zero } from "@ethersproject/constants"; 3 | import { calcBuyAmountInShares } from "./calcBuyAmountInShares"; 4 | import { divBN } from "../../utils"; 5 | 6 | const testCases: [[string, number, string[]], string][] = [[["1000000", 0, ["100000000", "100000000"]], "1970295"]]; 7 | 8 | describe("calcBuyAmountInShares", () => { 9 | it.each(testCases)( 10 | `should compute the amount of shares bought`, 11 | ([investmentAmount, outcomeIndex, poolBalances], expected) => { 12 | const result = calcBuyAmountInShares(investmentAmount, outcomeIndex, poolBalances, 0.01); 13 | 14 | expect(result).not.toBe(null); 15 | 16 | expect(divBN(result, expected)).toBeCloseTo(1); 17 | }, 18 | ); 19 | 20 | describe("when no holdings", () => { 21 | it("returns 0", () => expect(calcBuyAmountInShares(10, 0, [0, 0], 0.1)).toStrictEqual(Zero)); 22 | }); 23 | 24 | describe("when no funding", () => { 25 | it("returns 0", () => expect(calcBuyAmountInShares(0, 0, [100, 100], 0.1)).toStrictEqual(Zero)); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polymarket AMM Maths 2 | 3 | This package contains a number of functions to calculate the effects of interactions with the [Conditional Tokens Market Makers](https://github.com/Polymarket/conditional-tokens-market-makers) used by Polymarket. 4 | 5 | 6 | **Note:** A number of utility functions from https://github.com/protofire/omen-exchange have been included in this package which were then adapted where necessary. 7 | 8 | ## Running tests 9 | 10 | To run the tests, follow these steps. You must have at least node v10 and [yarn](https://yarnpkg.com/) installed. 11 | 12 | First clone the repository: 13 | 14 | ```sh 15 | git clone https://github.com/Polymarket/amm-maths.git 16 | ``` 17 | 18 | Move into the uniswap-sdk working directory 19 | 20 | ```sh 21 | cd amm-maths/ 22 | ``` 23 | 24 | Install dependencies 25 | 26 | ```sh 27 | yarn install 28 | ``` 29 | 30 | Run tests 31 | 32 | ```sh 33 | yarn test 34 | ``` 35 | 36 | You should see output like the following: 37 | 38 | ```sh 39 | yarn run v1.22.4 40 | $ jest 41 | PASS test/trading.test.ts 42 | PASS test/liquidity.test.ts 43 | PASS test/price.test.ts 44 | 45 | Test Suites: 3 passed, 3 total 46 | Tests: 64 passed, 64 total 47 | Snapshots: 0 total 48 | Time: 1.05 s 49 | Ran all test suites. 50 | Done in 1.64s. 51 | ``` 52 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterSharePurchase.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { Zero } from "@ethersproject/constants"; 3 | import { mulBN } from "../../utils"; 4 | import { computeBalanceAfterTrade } from "./computeBalanceAfterTrade"; 5 | 6 | /** 7 | * Computes the market maker's balances of outcome tokens after a trade to buy shares of a particular outcome 8 | * @param initialPoolBalances - the market maker's balances of outcome tokens before the trade 9 | * @param outcomeIndex - the index of the outcome token being bought 10 | * @param investmentAmountAfterFees - the amount of collateral being converted into outcome tokens (i.e. post fees) 11 | * @param sharesBoughtAmount - the amount of outcome tokens being removed from the market maker 12 | * @param fees - the percentage fees taken by the market maker on each trade 13 | */ 14 | export const computeBalanceAfterSharePurchase = ( 15 | initialPoolBalances: BigNumberish[], 16 | outcomeIndex: number, 17 | investmentAmount: BigNumberish, 18 | sharesBoughtAmount: BigNumberish, 19 | fees: number, 20 | ): BigNumber[] => 21 | computeBalanceAfterTrade( 22 | initialPoolBalances, 23 | outcomeIndex, 24 | fees !== 1 ? mulBN(investmentAmount, 1 - fees) : Zero, 25 | sharesBoughtAmount, 26 | ); 27 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterShareSale.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { WeiPerEther, Zero } from "@ethersproject/constants"; 3 | import { mulBN } from "../../utils"; 4 | import { computeBalanceAfterTrade } from "./computeBalanceAfterTrade"; 5 | 6 | /** 7 | * Computes the market maker's balances of outcome tokens after a trade to sell shares of a particular outcome 8 | * @param initialPoolBalances - the market maker's balances of outcome tokens before the trade 9 | * @param outcomeIndex - the index of the outcome token being bought 10 | * @param returnAmountBeforeFees - the amount of collateral being converted into outcome tokens (i.e. post fees) 11 | * @param sharesSoldAmount - the amount of outcome tokens being removed from the market maker 12 | * @param fees - the percentage fees taken by the market maker on each trade 13 | */ 14 | export const computeBalanceAfterShareSale = ( 15 | initialPoolBalances: BigNumberish[], 16 | outcomeIndex: number, 17 | returnAmount: BigNumberish, 18 | sharesSoldAmount: BigNumberish, 19 | fees: number, 20 | ): BigNumber[] => 21 | computeBalanceAfterTrade( 22 | initialPoolBalances, 23 | outcomeIndex, 24 | fees !== 1 25 | ? BigNumber.from(returnAmount) 26 | .mul(WeiPerEther) 27 | .div(WeiPerEther.sub(mulBN(WeiPerEther, fees))) 28 | .mul(-1) 29 | : Zero, 30 | BigNumber.from(sharesSoldAmount).mul(-1), 31 | ); 32 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { Zero } from "@ethersproject/constants"; 3 | 4 | /** 5 | * Performs multiplication between a BigNumber and a decimal number while temporarily scaling the decimal to preserve precision 6 | * @param a - a BigNumber to multiply by b 7 | * @param b - a decimal by which to multiple a by. 8 | * @param scale - the factor by which to scale the numerator by before division 9 | */ 10 | export const mulBN = (a: BigNumberish, b: number, scale = 10000): BigNumber => { 11 | return BigNumber.from(a) 12 | .mul(Math.round(b * scale)) 13 | .div(scale); 14 | }; 15 | 16 | /** 17 | * Performs division between two BigNumbers while temporarily scaling the numerator to preserve precision 18 | * @param a - the numerator 19 | * @param b - the denominator 20 | * @param scale - the factor by which to scale the numerator by before division 21 | */ 22 | export const divBN = (a: BigNumberish, b: BigNumberish, scale = 10000): number => { 23 | return ( 24 | BigNumber.from(a) 25 | .mul(scale) 26 | .div(b) 27 | .toNumber() / scale 28 | ); 29 | }; 30 | 31 | /** 32 | * Peforms ceil(numerator/denominator) 33 | * @param a - the numerator 34 | * @param b - the denominator 35 | */ 36 | export const ceilDiv = (a: BigNumberish, b: BigNumberish): BigNumber => { 37 | const aBN = BigNumber.from(a); 38 | return aBN.mod(b) === Zero ? aBN.div(b) : aBN.div(b).add(1); 39 | }; 40 | -------------------------------------------------------------------------------- /src/fpmm/trading/calcBuyAmountInShares.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { WeiPerEther as ONE, Zero } from "@ethersproject/constants"; 3 | import { ceilDiv, mulBN } from "../../utils"; 4 | 5 | /** 6 | * Computes the amount of shares that will be bought with `investmentAmount` amount collateral. 7 | * 8 | * @param investmentAmount The amount of collateral being put into the market maker 9 | * @param outcomeIndex The index of the outcome being bought 10 | * @param poolBalances How many tokens the market maker has of each outcome 11 | * @param fee The fee of the market maker, between 0 and 1 12 | */ 13 | export const calcBuyAmountInShares = ( 14 | investmentAmount: BigNumberish, 15 | outcomeIndex: number, 16 | poolBalances: BigNumberish[], 17 | fee: number, 18 | ): BigNumber => { 19 | if (outcomeIndex < 0 || outcomeIndex >= poolBalances.length) { 20 | throw new Error(`Outcome index '${outcomeIndex}' must be between 0 and '${poolBalances.length - 1}'`); 21 | } 22 | if (BigNumber.from(investmentAmount).isZero() || poolBalances.every(x => BigNumber.from(x).isZero())) return Zero; 23 | 24 | const investmentAmountMinusFees = mulBN(investmentAmount, 1 - fee); 25 | const newOutcomeBalance = poolBalances.reduce( 26 | (accumulator: BigNumber, poolBalance, i) => 27 | i !== outcomeIndex 28 | ? ceilDiv(accumulator.mul(poolBalance), BigNumber.from(poolBalance).add(investmentAmountMinusFees)) 29 | : accumulator.mul(poolBalance), 30 | ONE, 31 | ); 32 | 33 | return BigNumber.from(poolBalances[outcomeIndex]) 34 | .add(investmentAmountMinusFees) 35 | .sub(ceilDiv(newOutcomeBalance, ONE)); 36 | }; 37 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterTrade.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { computeBalanceAfterTrade } from "./computeBalanceAfterTrade"; 3 | 4 | const testCases: [[number[], number, number, number], number[]][] = [ 5 | [ 6 | [[100, 100], 0, 50, 100], 7 | [50, 150], 8 | ], 9 | [ 10 | [[100, 100], 1, 50, 100], 11 | [150, 50], 12 | ], 13 | [ 14 | [[100, 100, 100], 2, 50, 100], 15 | [150, 150, 50], 16 | ], 17 | ]; 18 | 19 | describe("computeBalanceAfterTrade", () => { 20 | it.each(testCases)( 21 | `should compute the right balance after trade`, 22 | ([holdings, outcomeIndex, collateral, shares], expected) => { 23 | const result = computeBalanceAfterTrade(holdings, outcomeIndex, collateral, shares); 24 | 25 | result.forEach((x, i) => expect(x.toNumber()).toBeCloseTo(expected[i])); 26 | }, 27 | ); 28 | 29 | describe("when index is negative", () => { 30 | it("throws", () => { 31 | expect(() => computeBalanceAfterTrade([100, 100, 100], -1, 50, 100)).toThrow(); 32 | }); 33 | }); 34 | 35 | describe("when index is equal to array's length", () => { 36 | it("throws", () => { 37 | expect(() => computeBalanceAfterTrade([100, 100, 100], 3, 50, 100)).toThrow(); 38 | }); 39 | }); 40 | 41 | describe("when index is bigger than array's length", () => { 42 | it("throws", () => { 43 | expect(() => computeBalanceAfterTrade([100, 100, 100], 10, 50, 100)).toThrow(); 44 | }); 45 | }); 46 | 47 | describe("when trade drains entirety of an outcome's balance", () => { 48 | it("throws", () => { 49 | expect(() => computeBalanceAfterTrade([100, 100, 100], 10, 0, 100)).toThrow(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterTrade.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { Zero } from "@ethersproject/constants"; 3 | 4 | /** 5 | * Computes the market maker's balances of outcome tokens after a trade 6 | * 7 | * @dev It is recommended to use the methods `computeBalanceAfterSharePurchase` and `computeBalanceAfterShareSale` instead of this 8 | * 9 | * @param initialPoolBalances - the market maker's balances of outcome tokens before the trade 10 | * @param outcomeIndex - the index of the outcome token being bought 11 | * @param investmentAmountAfterFees - the amount of collateral being converted into outcome tokens (i.e. post fees) 12 | * @param sharesBoughtAmount - the amount of outcome tokens being removed from the market maker 13 | */ 14 | export const computeBalanceAfterTrade = ( 15 | initialPoolBalances: BigNumberish[], 16 | outcomeIndex: number, 17 | investmentAmount: BigNumberish, 18 | sharesRemovedAmount: BigNumberish, 19 | ): BigNumber[] => { 20 | if (outcomeIndex < 0 || outcomeIndex >= initialPoolBalances.length) { 21 | throw new Error(`Outcome index '${outcomeIndex}' must be between 0 and '${initialPoolBalances.length - 1}'`); 22 | } 23 | 24 | // By default we treat a trade as a purchase of shares, sales can be treated as a purchase of a negative number of shares. 25 | const newPoolBalances = initialPoolBalances.map((h, i) => 26 | BigNumber.from(h) 27 | .add(investmentAmount) 28 | .sub(i === outcomeIndex ? sharesRemovedAmount : Zero), 29 | ); 30 | 31 | if (newPoolBalances.some(balance => balance.lte(0))) { 32 | throw new Error(`Trade is invalid: trade results in liquidity pool owning a negative number of tokens`); 33 | } 34 | 35 | return newPoolBalances; 36 | }; 37 | -------------------------------------------------------------------------------- /src/fpmm/trading/calcSellAmountInCollateral.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { BigNumber } from "@ethersproject/bignumber"; 3 | 4 | import { calcSellAmountInCollateral } from "./calcSellAmountInCollateral"; 5 | import { divBN } from "../../utils"; 6 | 7 | const testCases: [[string, number, string[], number], string][] = [ 8 | [["669745046301742827", 0, ["502512562814070351", "2000000000000000000"], 0.01], "496532989893612286"], 9 | [["669745046301742827", 1, ["2000000000000000000", "502512562814070351"], 0.01], "496532989893612286"], 10 | 11 | [["365128583991411574", 0, ["1502512562814070351", "673378000740715800"], 0.01], "100000000000000000"], 12 | [["148526984259244846", 0, ["673378000740715800", "1502512562814070351"], 0.01], "99336468831519624"], 13 | [ 14 | [ 15 | "169611024591650211", 16 | 2, 17 | ["1500000000000000000", "1500000000000000000", "299279122636316870", "1500000000000000000"], 18 | 0.01, 19 | ], 20 | "99437054864518193", 21 | ], 22 | [ 23 | ["18399816000000000000", 2, ["11009048601975904608", "17551468438676294710", "139733493703807763"], 0.01], 24 | "10381992534881175324", 25 | ], 26 | [["200000", 1, ["100000", "100000", "100000"], 0.01], "37815"], 27 | [["100000000", 0, ["18023", "156155227"], 0.02], "97968575"], 28 | [["1000000000", 0, ["18023", "156155227"], 0.02], "153028854"], 29 | ]; 30 | 31 | describe("calcSellAmountInCollateral", () => { 32 | it.each(testCases)( 33 | `should compute the amount of collateral to sell`, 34 | ([sharesToSell, outcomeIndex, poolBalances, fee], expected) => { 35 | const result = calcSellAmountInCollateral(sharesToSell, outcomeIndex, poolBalances, fee); 36 | 37 | expect(result).not.toBe(null); 38 | 39 | expect(divBN(result as BigNumber, BigNumber.from(expected))).toBeCloseTo(1); 40 | }, 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@polymarket/amm-maths", 3 | "version": "0.3.0", 4 | "description": "Utility package to store common maths for interacting with Conditional Tokens AMMs", 5 | "repository": "https://github.com/Polymarket/amm-maths.git", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "engines": { 9 | "node": ">=8", 10 | "npm": ">=5" 11 | }, 12 | "contributors": [ 13 | { 14 | "name": "Tom French", 15 | "url": "https://github.com/tomafrench" 16 | }, 17 | { 18 | "name": "Protofire", 19 | "url": "http://protofire.io" 20 | } 21 | ], 22 | "dependencies": { 23 | "@fvictorio/newton-raphson-method": "^1.0.5", 24 | "big.js": "^6.0.2", 25 | "lodash": "^4.17.20", 26 | "mathjs": "^8.0.1" 27 | }, 28 | "peerDependencies": { 29 | "@ethersproject/bignumber": "^5.0.8", 30 | "@ethersproject/constants": "^5.0.5" 31 | }, 32 | "devDependencies": { 33 | "@ethersproject/bignumber": "^5.0.8", 34 | "@ethersproject/constants": "^5.0.5", 35 | "@types/big.js": "^6.0.0", 36 | "@types/jest": "^26.0.20", 37 | "eslint": "6.x", 38 | "eslint-config-airbnb-base-typescript-prettier": "^4.1.0", 39 | "jest": "^26.6.3", 40 | "lint-staged": "^10.2.7", 41 | "prettier": "1.x", 42 | "shx": "^0.3.2", 43 | "ts-jest": "^26.4.4", 44 | "typescript": "^3.9.5" 45 | }, 46 | "keywords": [ 47 | "blockchain", 48 | "ethereum", 49 | "typescript", 50 | "conditional-tokens", 51 | "amm", 52 | "automatic-market-maker" 53 | ], 54 | "license": "MIT", 55 | "scripts": { 56 | "build": "tsc --project tsconfig.production.json", 57 | "clean": "shx rm -rf ./lib", 58 | "lint": "eslint --config ./.eslintrc.js --ignore-path ./.eslintignore ./src/**/*.ts", 59 | "test": "jest", 60 | "prepack": "yarn clean && yarn test && yarn build" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterSharePurchase.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { computeBalanceAfterSharePurchase } from "./computeBalanceAfterSharePurchase"; 3 | 4 | const testCases: [[number[], number, number, number, number], number[]][] = [ 5 | [ 6 | [[100, 100], 0, 50, 100, 0], 7 | [50, 150], 8 | ], 9 | [ 10 | [[100, 100], 1, 50, 100, 0], 11 | [150, 50], 12 | ], 13 | [ 14 | [[150, 150], 0, 50, 100, 1], 15 | [50, 150], 16 | ], 17 | [ 18 | [[100, 100], 0, 50, 100, 0.5], 19 | [25, 125], 20 | ], 21 | [ 22 | [[875, 875], 0, 500, 1000, 0.2], 23 | [275, 1275], 24 | ], 25 | 26 | [ 27 | [[100, 100, 100], 2, 50, 100, 0], 28 | [150, 150, 50], 29 | ], 30 | ]; 31 | 32 | describe("computeBalanceAfterSharePurchase", () => { 33 | it.each(testCases)( 34 | `should compute the right balance after sale of shares`, 35 | ([holdings, outcomeIndex, collateral, shares, fees], expected) => { 36 | const result = computeBalanceAfterSharePurchase(holdings, outcomeIndex, collateral, shares, fees); 37 | 38 | result.forEach((x, i) => expect(x.toNumber()).toBeCloseTo(expected[i])); 39 | }, 40 | ); 41 | 42 | describe("when index is negative", () => { 43 | it("throws", () => { 44 | expect(() => computeBalanceAfterSharePurchase([100, 100, 100], -1, 50, 100, 0)).toThrow(); 45 | }); 46 | }); 47 | 48 | describe("when index is equal to array's length", () => { 49 | it("throws", () => { 50 | expect(() => computeBalanceAfterSharePurchase([100, 100, 100], 3, 50, 100, 0)).toThrow(); 51 | }); 52 | }); 53 | 54 | describe("when index is bigger than array's length", () => { 55 | it("throws", () => { 56 | expect(() => computeBalanceAfterSharePurchase([100, 100, 100], 10, 50, 100, 0)).toThrow(); 57 | }); 58 | }); 59 | 60 | describe("when trade drains entirety of an outcome's balance", () => { 61 | it("throws", () => { 62 | expect(() => computeBalanceAfterSharePurchase([100, 100, 100], 10, 0, 100, 0)).toThrow(); 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/fpmm/liquidity/calcDistributionHint.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import Big from "big.js"; 3 | 4 | import { calcDistributionHint } from "../.."; 5 | 6 | const testCases: [number[], number[]][] = [ 7 | [ 8 | [60, 40], 9 | [816497, 1224745], 10 | ], 11 | [ 12 | [40, 60], 13 | [1224745, 816497], 14 | ], 15 | [ 16 | [15, 20, 65], 17 | [9309493, 6982120, 2148345], 18 | ], 19 | [ 20 | [15, 65, 20], 21 | [9309493, 2148345, 6982120], 22 | ], 23 | [ 24 | [20, 15, 65], 25 | [6982120, 9309493, 2148345], 26 | ], 27 | [ 28 | [20, 65, 15], 29 | [6982120, 2148345, 9309493], 30 | ], 31 | [ 32 | [65, 20, 15], 33 | [2148345, 6982120, 9309493], 34 | ], 35 | [ 36 | [65, 15, 20], 37 | [2148345, 9309493, 6982120], 38 | ], 39 | [ 40 | [10, 10, 10, 70], 41 | [26457513, 26457513, 26457513, 3779645], 42 | ], 43 | [ 44 | [10, 10, 70, 10], 45 | [26457513, 26457513, 3779645, 26457513], 46 | ], 47 | [ 48 | [10, 70, 10, 10], 49 | [26457513, 3779645, 26457513, 26457513], 50 | ], 51 | [ 52 | [70, 10, 10, 10], 53 | [3779645, 26457513, 26457513, 26457513], 54 | ], 55 | ]; 56 | 57 | describe("calcDistributionHint", () => { 58 | it.each(testCases)(`should compute the right distribution hint`, (odds, expectedHintsNumbers) => { 59 | const distributionHints = calcDistributionHint(odds).map(x => new Big(x.toString())); 60 | 61 | const distributionHintsMax = distributionHints.reduce((a, b) => (a.gt(b) ? a : b)); 62 | const distributionHintsScaled = distributionHints.map(dh => dh.div(distributionHintsMax)); 63 | 64 | const expectedHints = expectedHintsNumbers.map(x => new Big(x)); 65 | const expectedHintsMax = expectedHints.reduce((a, b) => (a.gt(b) ? a : b)); 66 | const expectedHintsScaled = expectedHints.map(eh => eh.div(expectedHintsMax)); 67 | 68 | distributionHintsScaled.forEach((dh, i) => 69 | expect(+dh.div(new Big(expectedHintsScaled[i])).toFixed()).toBeCloseTo(1), 70 | ); 71 | }); 72 | 73 | describe("when all the odds are equal", () => { 74 | const testCasesEqual: [number[]][] = [[[50, 50]], [[100 / 3, 100 / 3, 100 / 3]]]; 75 | it.each(testCasesEqual)("returns an empty array", distribution => { 76 | expect(calcDistributionHint(distribution)).toEqual([]); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/fpmm/trading/calcSellAmountInCollateral.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; 2 | import { newtonRaphson } from "@fvictorio/newton-raphson-method"; 3 | import Big from "big.js"; 4 | 5 | /** 6 | * Computes the amount of collateral that needs to be sold to get `shares` amount of shares. Returns null if the amount 7 | * couldn't be computed. 8 | * 9 | * @param sharesToSell The amount of shares that need to be sold 10 | * @param outcomeIndex The index of the outcome being bought 11 | * @param poolBalances How many tokens the market maker has of each outcome 12 | * @param fee The fee of the market maker, between 0 and 1 13 | */ 14 | export const calcSellAmountInCollateral = ( 15 | sharesToSell: BigNumberish, 16 | outcomeIndex: number, 17 | poolBalances: BigNumberish[], 18 | fee: number, 19 | ): BigNumber | null => { 20 | Big.DP = 90; 21 | 22 | if (outcomeIndex < 0 || outcomeIndex >= poolBalances.length) { 23 | throw new Error(`Outcome index '${outcomeIndex}' must be between 0 and '${poolBalances.length - 1}'`); 24 | } 25 | 26 | const holdings = poolBalances[outcomeIndex]; 27 | const otherHoldings = poolBalances.filter((_, i) => outcomeIndex !== i); 28 | 29 | const sharesToSellBig = new Big(sharesToSell.toString()); 30 | const holdingsBig = new Big(holdings.toString()); 31 | const otherHoldingsBig = otherHoldings.map(x => new Big(x.toString())); 32 | 33 | const f = (r: Big): Big => { 34 | // For three outcomes, where the first outcome is the one being sold, the formula is: 35 | // f(r) = ((y - R) * (z - R)) * (x + a - R) - x*y*z 36 | // where: 37 | // `R` is r / (1 - fee) 38 | // `x`, `y`, `z` are the market maker holdings for each outcome 39 | // `a` is the amount of outcomes that are being sold 40 | // `r` (the unknown) is the amount of collateral that will be returned in exchange of `a` tokens 41 | const R = r.div(1 - fee); 42 | const firstTerm = otherHoldingsBig.map(h => h.minus(R)).reduce((a, b) => a.mul(b)); 43 | const secondTerm = holdingsBig.plus(sharesToSellBig).minus(R); 44 | const thirdTerm = otherHoldingsBig.reduce((a, b) => a.mul(b), holdingsBig); 45 | return firstTerm.mul(secondTerm).minus(thirdTerm); 46 | }; 47 | 48 | const r = newtonRaphson(f, 0, { maxIterations: 100 }); 49 | 50 | if (r) { 51 | const amountToSell = BigNumber.from(r.toFixed(0)); 52 | return amountToSell; 53 | } 54 | 55 | return null; 56 | }; 57 | -------------------------------------------------------------------------------- /src/fpmm/trading/computeBalanceAfterShareSale.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env jest */ 2 | import { computeBalanceAfterShareSale } from "./computeBalanceAfterShareSale"; 3 | 4 | const testCases: [[string[], number, string, string, number], string[]][] = [ 5 | [ 6 | [["50", "150"], 0, "50", "100", 0], 7 | ["100", "100"], 8 | ], 9 | [ 10 | [["150", "50"], 1, "50", "100", 0], 11 | ["100", "100"], 12 | ], 13 | [ 14 | [["50", "150"], 0, "50", "100", 1], 15 | ["150", "150"], 16 | ], 17 | [ 18 | [["50", "150"], 0, "50", "100", 0.5], 19 | ["50", "50"], 20 | ], 21 | [ 22 | [["500", "1500"], 0, "500", "1000", 0.2], 23 | ["875", "875"], 24 | ], 25 | 26 | [ 27 | [["150", "150", "50"], 2, "50", "100", 0], 28 | ["100", "100", "100"], 29 | ], 30 | [ 31 | [["18023", "156155227"], 0, "97968575", "100000000", 0.02], 32 | ["50090", "56187294"], 33 | ], 34 | [ 35 | [["18023", "156155227"], 0, "153028854", "1000000000", 0.02], 36 | ["843866132", "3336"], 37 | ], 38 | [ 39 | [["3078621070814", "3788881"], 0, "1205", "998938116", 0.02], 40 | ["3079620007701", "3787652"], 41 | ], 42 | [ 43 | [["36569795948", "564314726"], 0, "743650", "49999926", 0.02], 44 | ["36619037048", "563555900"], 45 | ], 46 | ]; 47 | 48 | describe("computeBalanceAfterShareSale", () => { 49 | it.each(testCases)( 50 | `should compute the right balance after sale of shares`, 51 | ([holdings, outcomeIndex, collateral, shares, fees], expected) => { 52 | const result = computeBalanceAfterShareSale(holdings, outcomeIndex, collateral, shares, fees); 53 | 54 | result.forEach((x, i) => expect(x.toNumber()).toBeCloseTo(parseInt(expected[i], 10))); 55 | }, 56 | ); 57 | 58 | describe("when index is negative", () => { 59 | it("throws", () => { 60 | expect(() => computeBalanceAfterShareSale([100, 100, 100], -1, 50, 100, 0)).toThrow(); 61 | }); 62 | }); 63 | 64 | describe("when index is equal to array's length", () => { 65 | it("throws", () => { 66 | expect(() => computeBalanceAfterShareSale([100, 100, 100], 3, 50, 100, 0)).toThrow(); 67 | }); 68 | }); 69 | 70 | describe("when index is bigger than array's length", () => { 71 | it("throws", () => { 72 | expect(() => computeBalanceAfterShareSale([100, 100, 100], 10, 50, 100, 0)).toThrow(); 73 | }); 74 | }); 75 | 76 | describe("when trade drains entirety of an outcome's balance", () => { 77 | it("throws", () => { 78 | expect(() => computeBalanceAfterShareSale([100, 100, 100], 10, 0, 100, 0)).toThrow(); 79 | }); 80 | }); 81 | }); 82 | --------------------------------------------------------------------------------