Skip to content

Commit

Permalink
Proportional matches (#321)
Browse files Browse the repository at this point in the history
* add proportional match options

* move test utils to utils file

* add proportionalMatch test

* remove passportThreshold options from Calculator

* add proportional match options to Calculator options

* gdd proportional match logic

* remove options suffix from poportional matching options

* pmOptions to proportionalMatchOptions

* fix lint errors

* update getVotesWithCoefficients to receive an object

* extend test with sample data

* Update src/calculator/votes.ts

Co-authored-by: Mohamed Boudra <[email protected]>

* move var only use in a test

---------

Co-authored-by: Mohamed Boudra <[email protected]>
  • Loading branch information
gravityblast and boudra authored Oct 18, 2023
1 parent c3c5a83 commit ef95333
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 153 deletions.
21 changes: 11 additions & 10 deletions src/calculator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { PotentialVote } from "../http/api/v1/matches.js";
import { Price } from "../prices/common.js";
import { formatUnits, zeroAddress } from "viem";
import { ProportionalMatchOptions } from "./options.js";

export {
CalculatorError,
Expand Down Expand Up @@ -98,11 +99,11 @@ export type CalculatorOptions = {
roundId: string;
minimumAmountUSD?: number;
matchingCapAmount?: bigint;
passportThreshold?: number;
enablePassport?: boolean;
ignoreSaturation?: boolean;
overrides: Overrides;
chain: Chain;
proportionalMatch?: ProportionalMatchOptions;
};

export type AugmentedResult = Calculation & {
Expand All @@ -123,9 +124,9 @@ export default class Calculator {
private minimumAmountUSD: number | undefined;
private matchingCapAmount: bigint | undefined;
private enablePassport: boolean | undefined;
private passportThreshold: number | undefined;
private ignoreSaturation: boolean | undefined;
private overrides: Overrides;
private proportionalMatch?: ProportionalMatchOptions;

constructor(options: CalculatorOptions) {
this.passportProvider = options.passportProvider;
Expand All @@ -135,11 +136,11 @@ export default class Calculator {
this.roundId = options.roundId;
this.minimumAmountUSD = options.minimumAmountUSD;
this.enablePassport = options.enablePassport;
this.passportThreshold = options.passportThreshold;
this.matchingCapAmount = options.matchingCapAmount;
this.overrides = options.overrides;
this.ignoreSaturation = options.ignoreSaturation;
this.chain = options.chain;
this.proportionalMatch = options.proportionalMatch;
}

private votesWithCoefficientToContribution(
Expand Down Expand Up @@ -223,18 +224,18 @@ export default class Calculator {
10000n;
}

const votesWithCoefficients = await getVotesWithCoefficients(
this.chain,
const votesWithCoefficients = await getVotesWithCoefficients({
chain: this.chain,
round,
applications,
votes,
this.passportProvider,
{
passportProvider: this.passportProvider,
options: {
minimumAmountUSD: this.minimumAmountUSD,
enablePassport: this.enablePassport,
passportThreshold: this.passportThreshold,
}
);
},
proportionalMatchOptions: this.proportionalMatch,
});

const contributions: Contribution[] =
this.votesWithCoefficientToContribution(votesWithCoefficients);
Expand Down
21 changes: 21 additions & 0 deletions src/calculator/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type ProportionalMatchOptions = {
score: {
min: number;
max: number;
};
matchProportionPercentage: {
min: number;
max: number;
};
};

export const defaultProportionalMatchOptions: ProportionalMatchOptions = {
score: {
min: 15,
max: 25,
},
matchProportionPercentage: {
min: 50,
max: 100,
},
};
103 changes: 63 additions & 40 deletions src/calculator/votes.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
import { Chain } from "../config.js";
import type { Round, Application, Vote } from "../indexer/types.js";
import type { PassportScore, PassportProvider } from "../passport/index.js";
import { ProportionalMatchOptions } from "./options.js";
import { defaultProportionalMatchOptions } from "./options.js";

export type VoteWithCoefficient = Vote & {
coefficient: number;
passportScore?: PassportScore;
};

/* TODO: ripe for a functional rewrite, also: https://massimilianomirra.com/notes/the-dangers-of-greedy-functions */
export async function getVotesWithCoefficients(
chain: Chain,
round: Round,
applications: Array<Application>,
votes: Array<Vote>,
passportProvider: PassportProvider,
interface GetVotesWithCoefficientsArgs {
chain: Chain;
round: Round;
applications: Array<Application>;
votes: Array<Vote>;
passportProvider: PassportProvider;
options: {
minimumAmountUSD?: number;
enablePassport?: boolean;
passportThreshold?: number;
}
};
proportionalMatchOptions?: ProportionalMatchOptions;
}

/* TODO: ripe for a functional rewrite, also: https://massimilianomirra.com/notes/the-dangers-of-greedy-functions */
export async function getVotesWithCoefficients(
args: GetVotesWithCoefficientsArgs
): Promise<Array<VoteWithCoefficient>> {
const applicationMap = applications.reduce(
const applicationMap = args.applications.reduce(
(map, application) => {
map[application.id] = application;
return map;
Expand All @@ -29,18 +35,18 @@ export async function getVotesWithCoefficients(
);

const enablePassport =
options.enablePassport ??
round?.metadata?.quadraticFundingConfig?.sybilDefense ??
args.options.enablePassport ??
args.round?.metadata?.quadraticFundingConfig?.sybilDefense ??
false;

const minimumAmountUSD = Number(
options.minimumAmountUSD ??
round.metadata?.quadraticFundingConfig?.minDonationThresholdAmount ??
args.options.minimumAmountUSD ??
args.round.metadata?.quadraticFundingConfig?.minDonationThresholdAmount ??
0
);

const votePromises = votes.map(async (originalVote) => {
const vote = applyVoteCap(chain, originalVote);
const votePromises = args.votes.map(async (originalVote) => {
const vote = applyVoteCap(args.chain, originalVote);
const voter = vote.voter.toLowerCase();
const application = applicationMap[vote.applicationId];

Expand All @@ -60,35 +66,27 @@ export async function getVotesWithCoefficients(
return [];
}

// Passport check
// We start setting the coefficient to 1, keeping 100% of the donation matching power
let coefficient = 1;

const passportScore = await passportProvider.getScoreByAddress(voter);
let passportCheckPassed = false;

if (enablePassport) {
if (
options.passportThreshold &&
Number(passportScore?.evidence?.rawScore ?? "0") >
options.passportThreshold
) {
passportCheckPassed = true;
} else if (passportScore?.evidence?.success) {
passportCheckPassed = true;
}
} else {
passportCheckPassed = true;
// Minimum donation amount check
const minAmountCheckPassed = vote.amountUSD >= minimumAmountUSD;
// We don't consider the donation if it's lower than the minimum amount
if (!minAmountCheckPassed) {
coefficient = 0;
}

// Minimum amount check

let minAmountCheckPassed = false;

if (vote.amountUSD >= minimumAmountUSD) {
minAmountCheckPassed = true;
// Passport check
const passportScore = await args.passportProvider.getScoreByAddress(voter);
if (minAmountCheckPassed && enablePassport) {
// Set to 0 if the donor doesn't have a passport
const rawScore = Number(passportScore?.evidence?.rawScore ?? "0");
coefficient = scoreToCoefficient(
args.proportionalMatchOptions ?? defaultProportionalMatchOptions,
rawScore
);
}

const coefficient = passportCheckPassed && minAmountCheckPassed ? 1 : 0;

return [
{
...vote,
Expand All @@ -102,6 +100,31 @@ export async function getVotesWithCoefficients(
return (await Promise.all(votePromises)).flat();
}

function scoreToCoefficient(
options: ProportionalMatchOptions,
score: number
) {
if (score < options.score.min) {
return 0;
}

if (score > options.score.max) {
return 1;
}

const shiftedMax = options.score.max - options.score.min;
const shiftedScore = score - options.score.min;

const perc =
options.matchProportionPercentage.min +
((options.matchProportionPercentage.max -
options.matchProportionPercentage.min) *
shiftedScore) /
shiftedMax;

return perc / 100;
}

export function applyVoteCap(chain: Chain, vote: Vote): Vote {
const tokenConfig = chain.tokens.find(
(t) => t.address.toLowerCase() === vote.token.toLowerCase()
Expand Down
10 changes: 5 additions & 5 deletions src/http/api/v1/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ export const createHandler = (config: HttpApiConfig): express.Router => {
throw new Error(`Chain ${chainId} not configured`);
}

const votesWithCoefficients = await getVotesWithCoefficients(
chainConfig,
const votesWithCoefficients = await getVotesWithCoefficients({
chain: chainConfig,
round,
applications,
votes,
config.passportProvider,
{}
);
passportProvider: config.passportProvider,
options: {},
});

const records = votesWithCoefficients.flatMap((vote) => {
return [
Expand Down
4 changes: 0 additions & 4 deletions src/http/api/v1/matches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export const createHandler = (config: HttpApiConfig): express.Router => {
const roundId = req.params.roundId;

const minimumAmountUSD = req.query.minimumAmountUSD?.toString();
const passportThreshold = req.query.passportThreshold?.toString();
const matchingCapAmount = req.query.matchingCapAmount?.toString();

const enablePassport = boolParam(req.query, "enablePassport");
Expand Down Expand Up @@ -94,9 +93,6 @@ export const createHandler = (config: HttpApiConfig): express.Router => {
matchingCapAmount: matchingCapAmount
? BigInt(matchingCapAmount)
: undefined,
passportThreshold: passportThreshold
? Number(passportThreshold)
: undefined,
enablePassport: enablePassport,
ignoreSaturation: ignoreSaturation,
overrides,
Expand Down
Loading

0 comments on commit ef95333

Please sign in to comment.