-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathrelayFeeCalculator.ts
148 lines (135 loc) · 7.14 KB
/
relayFeeCalculator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import assert from "assert";
import * as uma from "@uma/sdk";
import { BigNumber } from "ethers";
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
export interface QueryInterface {
getGasCosts: (tokenSymbol: string) => Promise<BigNumberish>;
getTokenPrice: (tokenSymbol: string) => Promise<number | string>;
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;
}
export class RelayFeeCalculator {
private queries: Required<RelayFeeCalculatorConfig>["queries"];
private gasDiscountPercent: Required<RelayFeeCalculatorConfig>["gasDiscountPercent"];
private capitalDiscountPercent: Required<RelayFeeCalculatorConfig>["capitalDiscountPercent"];
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.capitalDiscountPercent || 0;
this.feeLimitPercent = config.feeLimitPercent || 0;
this.nativeTokenDecimals = config.nativeTokenDecimals || 18;
this.capitalCostsPercent = config.capitalCostsPercent || 0;
assert(
this.gasDiscountPercent >= 0 && this.gasDiscountPercent <= 100,
"gasDiscountPercent must be between 0 and 100 percent"
);
assert(
this.capitalDiscountPercent >= 0 && this.capitalDiscountPercent <= 100,
"capitalDiscountPercent must be between 0 and 100 percent"
);
assert(
this.feeLimitPercent >= 0 && this.feeLimitPercent <= 100,
"feeLimitPercent must be between 0 and 100 percent"
);
assert(
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),
this.queries.getTokenPrice(tokenSymbol),
this.queries.getTokenDecimals(tokenSymbol),
]);
const gasFeesInToken = nativeToToken(gasCosts, tokenPrice, decimals, this.nativeTokenDecimals);
return percent(gasFeesInToken, amountToRelay);
}
// 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> {
// 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;
const gasFeePercent = await this.gasFeePercent(amountToRelay, tokenSymbol);
const gasFeeTotal = gasFeePercent.mul(amountToRelay).div(fixedPointAdjustment);
const capitalFeePercent = await this.capitalFeePercent(amountToRelay, tokenSymbol);
const capitalFeeTotal = capitalFeePercent.mul(amountToRelay).div(fixedPointAdjustment);
const relayFeePercent = gasFeePercent.add(capitalFeePercent);
const relayFeeTotal = gasFeeTotal.add(capitalFeeTotal);
if (this.feeLimitPercent) {
isAmountTooLow = gasFeePercent.add(capitalFeePercent).gt(toBNWei(this.feeLimitPercent / 100));
}
return {
amountToRelay: amountToRelay.toString(),
tokenSymbol,
gasFeePercent: gasFeePercent.toString(),
gasFeeTotal: gasFeeTotal.toString(),
gasDiscountPercent: this.gasDiscountPercent,
capitalFeePercent: capitalFeePercent.toString(),
capitalFeeTotal: capitalFeeTotal.toString(),
capitalDiscountPercent: this.capitalDiscountPercent,
relayFeePercent: relayFeePercent.toString(),
relayFeeTotal: relayFeeTotal.toString(),
feeLimitPercent: this.feeLimitPercent,
isAmountTooLow,
};
}
}