Skip to content

Commit

Permalink
feat: Make relayer fee curve more dynamic (#55)
Browse files Browse the repository at this point in the history
* feat: Truncate realized LP fee %'s to match UMIP

* feat: add more dynamic relayer fee curve

* Update relayFeeCalculator.ts

* make validation logic static

Signed-off-by: nicholaspai <[email protected]>

* validate upon construction

* Update package.json
  • Loading branch information
nicholaspai authored Jun 16, 2022
1 parent 7253926 commit 7905929
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@across-protocol/sdk-v2",
"author": "UMA Team",
"version": "0.1.12",
"version": "0.1.13",
"license": "AGPL-3.0",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
110 changes: 107 additions & 3 deletions src/relayFeeCalculator/relayFeeCalculator.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
import assert from "assert";
import dotenv from "dotenv";
import { RelayFeeCalculator, QueryInterface } from "./relayFeeCalculator";
import { gasCost, BigNumberish } from "../utils";
import { gasCost, BigNumberish, toBNWei } from "../utils";

dotenv.config({ path: ".env" });

const testCapitalCostsConfig: { [token: string]: any } = {
WBTC: {
lowerBound: toBNWei("0.0003").toString(),
upperBound: toBNWei("0.002").toString(),
cutoff: toBNWei("15").toString(),
decimals: 8,
},
DAI: {
lowerBound: toBNWei("0.0003").toString(),
upperBound: toBNWei("0.0015").toString(),
cutoff: toBNWei("500000").toString(),
decimals: 18,
},
};

// Example of how to write this query class
class ExampleQueries implements QueryInterface {
constructor(private defaultGas = "305572") {}
Expand All @@ -20,12 +35,101 @@ class ExampleQueries implements QueryInterface {
}
describe("RelayFeeCalculator", () => {
let client: RelayFeeCalculator;
let queries: ExampleQueries;
beforeAll(() => {
const queries = new ExampleQueries();
client = new RelayFeeCalculator({ queries });
queries = new ExampleQueries();
});
it("relayerFeeDetails", async () => {
client = new RelayFeeCalculator({ queries });
const result = await client.relayerFeeDetails(100000000, "usdc");
assert.ok(result);
});
it("capitalFeePercent", async () => {
// Invalid capital cost configs throws on construction:
assert.throws(
() =>
new RelayFeeCalculator({
queries,
capitalCostsConfig: {
WBTC: { ...testCapitalCostsConfig["WBTC"], upperBound: toBNWei("0.01").toString() },
},
}),
/upper bound must be </
);
assert.throws(
() =>
RelayFeeCalculator.validateCapitalCostsConfig({
...testCapitalCostsConfig["WBTC"],
upperBound: toBNWei("0.01").toString(),
}),
/upper bound must be </
);
assert.throws(
() =>
new RelayFeeCalculator({
queries,
capitalCostsConfig: {
WBTC: {
...testCapitalCostsConfig["WBTC"],
upperBound: toBNWei("0.001").toString(),
lowerBound: toBNWei("0.002").toString(),
},
},
}),
/lower bound must be <= upper bound/
);
assert.throws(
() =>
RelayFeeCalculator.validateCapitalCostsConfig({
...testCapitalCostsConfig["WBTC"],
upperBound: toBNWei("0.001").toString(),
lowerBound: toBNWei("0.002").toString(),
}),
/lower bound must be <= upper bound/
);
assert.throws(
() =>
new RelayFeeCalculator({
queries,
capitalCostsConfig: { WBTC: { ...testCapitalCostsConfig["WBTC"], decimals: 0 } },
}),
/invalid decimals/
);
assert.throws(
() => RelayFeeCalculator.validateCapitalCostsConfig({ ...testCapitalCostsConfig["WBTC"], decimals: 0 }),
/invalid decimals/
);
assert.throws(
() =>
new RelayFeeCalculator({
queries,
capitalCostsConfig: { WBTC: { ...testCapitalCostsConfig["WBTC"], decimals: 19 } },
}),
/invalid decimals/
);
assert.throws(
() => RelayFeeCalculator.validateCapitalCostsConfig({ ...testCapitalCostsConfig["WBTC"], decimals: 19 }),
/invalid decimals/
);
const client = new RelayFeeCalculator({
queries,
capitalCostsConfig: testCapitalCostsConfig,
capitalCostsPercent: 0.01,
});

// If token doesn't have a config set, then returns default fixed fee %:
assert.equal((await client.capitalFeePercent(toBNWei("1"), "UNKNOWN")).toString(), toBNWei("0.0001").toString());

// Test with different decimals:

// Amount near zero should charge slightly more than lower bound
assert.equal((await client.capitalFeePercent(toBNWei("0.001", 8), "WBTC")).toString(), "300056666666000");
assert.equal((await client.capitalFeePercent(toBNWei("1"), "DAI")).toString(), "300001200000000");
// Amount right below cutoff should charge slightly below 1/2 of (lower bound + upper bound)
assert.equal((await client.capitalFeePercent(toBNWei("14.999", 8), "WBTC")).toString(), "1149943333333330");
assert.equal((await client.capitalFeePercent(toBNWei("499999"), "DAI")).toString(), "899998800000000");
// Amount >>> than cutoff should charge slightly below upper bound
assert.equal((await client.capitalFeePercent(toBNWei("600", 8), "WBTC")).toString(), "1978749999999999");
assert.equal((await client.capitalFeePercent(toBNWei("20000000"), "DAI")).toString(), "1485000000000000");
});
});
58 changes: 55 additions & 3 deletions src/relayFeeCalculator/relayFeeCalculator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from "assert";
import * as uma from "@uma/sdk";
import { BigNumber } from "ethers";
import { BigNumberish, toBNWei, nativeToToken } from "../utils";
import { BigNumberish, toBNWei, nativeToToken, toBN, min, max } from "../utils";
const { percent, fixedPointAdjustment } = uma.across.utils;

// This needs to be implemented for every chain and passed into RelayFeeCalculator
Expand All @@ -11,12 +11,20 @@ export interface QueryInterface {
getTokenDecimals: (tokenSymbol: string) => Promise<number>;
}

export const expectedCapitalCostsKeys = ["lowerBound", "upperBound", "cutoff", "decimals"];
export interface CapitalCostConfig {
lowerBound: string;
upperBound: string;
cutoff: string;
decimals: number;
}
export interface RelayFeeCalculatorConfig {
nativeTokenDecimals?: number;
gasDiscountPercent?: number;
capitalDiscountPercent?: number;
feeLimitPercent?: number;
capitalCostsPercent?: number;
capitalCostsConfig?: { [token: string]: CapitalCostConfig };
queries: QueryInterface;
}

Expand All @@ -27,10 +35,11 @@ export class RelayFeeCalculator {
private feeLimitPercent: Required<RelayFeeCalculatorConfig>["feeLimitPercent"];
private nativeTokenDecimals: Required<RelayFeeCalculatorConfig>["nativeTokenDecimals"];
private capitalCostsPercent: Required<RelayFeeCalculatorConfig>["capitalCostsPercent"];
private capitalCostsConfig: Required<RelayFeeCalculatorConfig>["capitalCostsConfig"];
constructor(config: RelayFeeCalculatorConfig) {
this.queries = config.queries;
this.gasDiscountPercent = config.gasDiscountPercent || 0;
this.capitalDiscountPercent = config.capitalCostsPercent || 0;
this.capitalDiscountPercent = config.capitalDiscountPercent || 0;
this.feeLimitPercent = config.feeLimitPercent || 0;
this.nativeTokenDecimals = config.nativeTokenDecimals || 18;
this.capitalCostsPercent = config.capitalCostsPercent || 0;
Expand All @@ -50,7 +59,18 @@ export class RelayFeeCalculator {
this.capitalCostsPercent >= 0 && this.capitalCostsPercent <= 100,
"capitalCostsPercent must be between 0 and 100 percent"
);
this.capitalCostsConfig = config.capitalCostsConfig || {};
for (const token of Object.keys(this.capitalCostsConfig)) {
RelayFeeCalculator.validateCapitalCostsConfig(this.capitalCostsConfig[token]);
}
}

static validateCapitalCostsConfig(capitalCosts: CapitalCostConfig) {
assert(toBN(capitalCosts.upperBound).lt(toBNWei("0.01")), "upper bound must be < 1%");
assert(toBN(capitalCosts.lowerBound).lte(capitalCosts.upperBound), "lower bound must be <= upper bound");
assert(capitalCosts.decimals > 0 && capitalCosts.decimals <= 18, "invalid decimals");
}

async gasFeePercent(amountToRelay: BigNumberish, tokenSymbol: string): Promise<BigNumber> {
const [gasCosts, tokenPrice, decimals] = await Promise.all([
this.queries.getGasCosts(tokenSymbol),
Expand All @@ -64,7 +84,39 @@ export class RelayFeeCalculator {
// Note: these variables are unused now, but may be needed in future versions of this function that are more complex.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async capitalFeePercent(_amountToRelay: BigNumberish, _tokenSymbol: string): Promise<BigNumber> {
return toBNWei(this.capitalCostsPercent / 100);
// V0: Charge fixed capital fee
const defaultFee = toBNWei(this.capitalCostsPercent / 100);

// V1: Charge fee that scales with size. This will charge a fee % based on a linear fee curve with a "kink" at a
// cutoff in the same units as _amountToRelay. Before the kink, the fee % will increase linearly from a lower
// bound to an upper bound. After the kink, the fee % increase will be fixed, and slowly approach the upper bound
// for very large amount inputs.
if (this.capitalCostsConfig[_tokenSymbol]) {
const config = this.capitalCostsConfig[_tokenSymbol];
// Scale amount "y" to 18 decimals
const y = toBN(_amountToRelay).mul(toBNWei("1", 18 - config.decimals));
// At a minimum, the fee will be equal to lower bound fee * y
const minCharge = toBN(config.lowerBound).mul(y).div(fixedPointAdjustment);

// Charge an increasing marginal fee % up to min(cutoff, y). If y is very close to the cutoff, the fee %
// will be equal to half the sum of (upper bound + lower bound).
const yTriangle = min(config.cutoff, y);

// triangleSlope is slope of fee curve from lower bound to upper bound.
// triangleCharge is interval of curve from 0 to y for curve = triangleSlope * y
const triangleSlope = toBN(config.upperBound).sub(config.lowerBound).mul(fixedPointAdjustment).div(config.cutoff);
const triangleHeight = triangleSlope.mul(yTriangle).div(fixedPointAdjustment);
const triangleCharge = triangleHeight.mul(yTriangle).div(toBNWei(2));

// For any amounts above the cutoff, the marginal fee % will not increase but will be fixed at the upper bound
// value.
const yRemainder = max(toBN(0), y.sub(config.cutoff));
const remainderCharge = yRemainder.mul(toBN(config.upperBound).sub(config.lowerBound)).div(fixedPointAdjustment);

return minCharge.add(triangleCharge).add(remainderCharge).mul(fixedPointAdjustment).div(y);
}

return defaultFee;
}
async relayerFeeDetails(amountToRelay: BigNumberish, tokenSymbol: string) {
let isAmountTooLow = false;
Expand Down

0 comments on commit 7905929

Please sign in to comment.