Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Fees & Oracle #3

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/chain/src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { SetDelegateProposal } from "./governance/set-delegate-proposal";
import { OutgoingMessages } from "./outgoing-messages";
import { TokenRegistry } from "./token-registry";
import { XYK } from "./xyk/xyk";
import { VolumeOracle } from "./xyk/VolumeOracle";

export const modules = {
Faucet,
Locks,
VolumeOracle,
Balances: Balances,
SetDelegateProposal,
OutgoingMessages,
Expand All @@ -36,8 +38,11 @@ export const config: ModulesConfig<
TokenRegistry: {},
XYK: {
feeDivider: 1000n,
fee: 3n, //
fee: 3n, // 0.3%
// volumeFee: 0n,
volumeFee: 500n, // 500% at max volatility
},
VolumeOracle: {}
};

export default {
Expand Down
66 changes: 66 additions & 0 deletions packages/chain/src/runtime/xyk/VolumeOracle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { runtimeModule, RuntimeModule, state } from "@proto-kit/module";
import { TokenId, UInt64 } from "@proto-kit/library";
import { PoolKey } from "./pool-key";
import { Bool, Field, Int64, Provable, PublicKey, Sign, Struct } from "o1js";
import { State, StateMap } from "@proto-kit/protocol";

export class PoolBlockPair extends Struct({
pool: PoolKey,
block: UInt64,
}) {}

@runtimeModule()
export class VolumeOracle extends RuntimeModule {
@state() cumulativeVolume = StateMap.from(PoolBlockPair, UInt64);
@state() netVolume = StateMap.from<PoolBlockPair, Int64>(
PoolBlockPair,
Int64,
);

public getAverage(pool: PoolKey, numValues: number = 3) {
const values: Int64[] = [];
let currentBlock = new UInt64(this.network.block.height.value);
for (let i = 0; i < numValues; i++) {
values.push(this.netVolume.get({ pool, block: currentBlock }).orElse(Int64.zero));

currentBlock = currentBlock.sub(
new UInt64(Provable.if(currentBlock.equals(0), Field(0), Field(1))),
);
}

return values.reduce((a, b) => a.add(b)).div(numValues);
}

public addVolume(
pool: PoolKey,
direction: Bool,
amountA: UInt64,
): {
cumulativeVolume: UInt64;
netVolume: Int64;
} {
const oracleKey = new PoolBlockPair({
pool,
block: UInt64.from(this.network.block.height),
});

const volumeBefore = new UInt64(
this.cumulativeVolume.get(oracleKey).orElse(UInt64.from(0)).value,
);
const cumulativeAfter = volumeBefore.add(amountA);
this.cumulativeVolume.set(oracleKey, cumulativeAfter);

const netBefore = Int64.from(
this.netVolume.get(oracleKey).orElse(Int64.from(0)),
);
const amountInt = Int64.fromField(amountA.value);
const netChange = Provable.if(direction, amountInt, amountInt.neg());
const netAfter = netBefore.add(netChange);
this.netVolume.set(oracleKey, netAfter);

return {
cumulativeVolume: cumulativeAfter,
netVolume: netAfter,
};
}
}
108 changes: 77 additions & 31 deletions packages/chain/src/runtime/xyk/xyk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TokenPair } from "./token-pair";
import { LPTokenId } from "./lp-token-id";
import { MAX_TOKEN_ID, TokenRegistry } from "../token-registry";
import { Balances } from "../balances";
import { VolumeOracle } from "./VolumeOracle";

export const errors = {
tokensNotDistinct: () => `Tokens must be different`,
Expand All @@ -26,7 +27,7 @@ export const errors = {
amountOutIsInsufficient: () => `Amount out is insufficient`,
};

// we need a placeholder pool value until protokit supports value-less dictonaries or state arrays
// we need a placeholder pool value until protokit supports value-less dictionaries or state arrays
export const placeholderPoolValue = Bool(true);

export const MAX_PATH_LENGTH = 3;
Expand All @@ -41,6 +42,12 @@ export class TokenIdPath extends Struct({
export interface XYKConfig {
feeDivider: bigint;
fee: bigint;
// Volume fee, maximum percentage fee for 100% relative volatility
volumeFee: bigint;

// // Percentage of the pool's token reverses that have to be traded each block for 1 volumeFee to be applied extra (scaled by feeDivider)
// // 100% = 1e9
// volumeDivider: bigint;
}

/**
Expand All @@ -57,7 +64,8 @@ export class XYK extends RuntimeModule<XYKConfig> {
*/
public constructor(
@inject("Balances") public balances: Balances,
@inject("TokenRegistry") public tokenRegistry: TokenRegistry
@inject("TokenRegistry") public tokenRegistry: TokenRegistry,
@inject("VolumeOracle") public volumeOracle: VolumeOracle,
) {
super();
}
Expand All @@ -81,7 +89,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenAId: TokenId,
tokenBId: TokenId,
tokenAAmount: Balance,
tokenBAmount: Balance
tokenBAmount: Balance,
) {
const tokenPair = TokenPair.from(tokenAId, tokenBId);
const poolKey = PoolKey.fromTokenPair(tokenPair);
Expand All @@ -104,15 +112,15 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenAId.greaterThan(tokenBId),
Balance,
tokenAAmount,
tokenBAmount
).value
tokenBAmount,
).value,
);

this.tokenRegistry.addTokenId(lpTokenId);
this.balances.mintAndIncrementSupply(
lpTokenId,
creator,
initialLPTokenSupply
initialLPTokenSupply,
);
this.pools.set(poolKey, placeholderPoolValue);
}
Expand All @@ -132,7 +140,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenAId: TokenId,
tokenBId: TokenId,
tokenAAmount: Balance,
tokenBAmountLimit: Balance
tokenBAmountLimit: Balance,
) {
const tokenPair = TokenPair.from(tokenAId, tokenBId);
// tokenAId = tokenPair.tokenAId;
Expand All @@ -145,7 +153,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
const reserveB = this.balances.getBalance(tokenBId, poolKey);
const reserveANotZero = reserveA.greaterThan(Balance.from(0));
const adjustedReserveA = Balance.from(
Provable.if(reserveANotZero, reserveA.value, Balance.from(1).value)
Provable.if(reserveANotZero, reserveA.value, Balance.from(1).value),
);

// TODO: why do i need Balance.from on the `amountA` argument???
Expand Down Expand Up @@ -181,7 +189,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
lpTokenAmount: Balance,
// TODO: change to min/max limits everywhere
tokenAAmountLimit: Balance,
tokenBLAmountLimit: Balance
tokenBLAmountLimit: Balance,
) {
const tokenPair = TokenPair.from(tokenAId, tokenBId);
tokenAId = tokenPair.tokenAId;
Expand All @@ -195,8 +203,8 @@ export class XYK extends RuntimeModule<XYKConfig> {
Provable.if(
lpTokenTotalSupplyIsZero,
Balance.from(1).value,
lpTokenTotalSupply.value
)
lpTokenTotalSupply.value,
),
);
const reserveA = this.balances.getBalance(tokenAId, poolKey);
const reserveB = this.balances.getBalance(tokenBId, poolKey);
Expand Down Expand Up @@ -233,15 +241,15 @@ export class XYK extends RuntimeModule<XYKConfig> {
public calculateTokenOutAmountFromReserves(
reserveIn: Balance,
reserveOut: Balance,
amountIn: Balance
amountIn: Balance,
) {
const numerator = amountIn.mul(reserveOut);
const denominator = reserveIn.add(amountIn);

// TODO: extract to safemath
const adjustedDenominator = Balance.from(
Provable.if(denominator.equals(0), Balance, Balance.from(1), denominator)
.value
.value,
);

assert(denominator.equals(adjustedDenominator), "denominator is zero");
Expand All @@ -252,7 +260,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
public calculateTokenOutAmount(
tokenIn: TokenId,
tokenOut: TokenId,
amountIn: Balance
amountIn: Balance,
) {
const tokenPair = TokenPair.from(tokenIn, tokenOut);
const pool = PoolKey.fromTokenPair(tokenPair);
Expand All @@ -263,14 +271,14 @@ export class XYK extends RuntimeModule<XYKConfig> {
return this.calculateTokenOutAmountFromReserves(
reserveIn,
reserveOut,
amountIn
amountIn,
);
}

public calculateAmountIn(
tokenIn: TokenId,
tokenOut: TokenId,
amountOut: Balance
amountOut: Balance,
) {
const tokenPair = TokenPair.from(tokenIn, tokenOut);
const pool = PoolKey.fromTokenPair(tokenPair);
Expand All @@ -284,15 +292,15 @@ export class XYK extends RuntimeModule<XYKConfig> {
public calculateAmountInFromReserves(
reserveIn: Balance,
reserveOut: Balance,
amountOut: Balance
amountOut: Balance,
) {
const numerator = reserveIn.mul(amountOut);
const denominator = reserveOut.sub(amountOut);

// TODO: extract to safemath
const adjustedDenominator = Balance.from(
Provable.if(denominator.equals(0), Balance, Balance.from(1), denominator)
.value
.value,
);

assert(denominator.equals(adjustedDenominator), "denominator is zero");
Expand All @@ -304,7 +312,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
seller: PublicKey,
{ path }: TokenIdPath,
amountIn: Balance,
amountOutMinLimit: Balance
amountOutMinLimit: Balance,
) {
const initialTokenPair = TokenPair.from(path[0], path[1]);
const initialPoolKey = PoolKey.fromTokenPair(initialTokenPair);
Expand All @@ -331,23 +339,61 @@ export class XYK extends RuntimeModule<XYKConfig> {
const calculatedAmountOut = this.calculateTokenOutAmount(
tokenIn,
tokenOut,
Balance.from(amountIn)
Balance.from(amountIn),
);

const amoutOutWithoutFee = calculatedAmountOut.sub(
calculatedAmountOut.mul(3n).div(100000n)
this.volumeOracle.addVolume(
poolKey,
tokenPair.tokenAId.equals(tokenIn),
calculatedAmountOut,
);

const { fee, volumeFee, feeDivider } = this.config;

// Volatility-based fee calculation
const averageVolatility = UInt64.from(
this.volumeOracle.getAverage(poolKey, 3).magnitude,
);
const reservesTokenA = this.balances.getBalance(
tokenPair.tokenAId,
poolKey,
);

const reservesTokenASafe = Provable.if(
reservesTokenA.equals(0),
Balance,
Balance.from(1),
reservesTokenA,
);
assert(
reservesTokenASafe.value
.equals(reservesTokenA.value)
// For the case that this loop run is a dummy
.or(tokenPair.tokenAId.equals(MAX_TOKEN_ID)),
"Reserves empty",
);

// Volatility percentage [1e9, 0]
const relativeVolatility = averageVolatility
.mul(1e9)
.div(new Balance(reservesTokenASafe.value));
// Fee base percentage, represented as fee scaled by feeDivider
const volatilityFeeBase = relativeVolatility.mul(volumeFee).div(1e9);

const amountOutWithoutFee = calculatedAmountOut
.sub(calculatedAmountOut.mul(fee).div(feeDivider))
.sub(calculatedAmountOut.mul(volatilityFeeBase).div(feeDivider));

lastTokenOut = Provable.if(poolExists, TokenId, tokenOut, lastTokenOut);

lastPoolKey = Provable.if(poolExists, PoolKey, poolKey, lastPoolKey);

amountOut = Balance.from(
Provable.if(poolExists, Balance, amoutOutWithoutFee, amountOut).value
Provable.if(poolExists, Balance, amountOutWithoutFee, amountOut).value,
);

amountIn = UInt64.from(
Provable.if(poolExists, Balance, amountIn, Balance.zero).value
Provable.if(poolExists, Balance, amountIn, Balance.zero).value,
);

this.balances.transfer(tokenIn, sender, lastPoolKey, amountIn);
Expand All @@ -369,7 +415,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenAId: TokenId,
tokenBId: TokenId,
tokenAAmount: Balance,
tokenBAmount: Balance
tokenBAmount: Balance,
) {
const creator = this.transaction.sender.value;
this.createPool(creator, tokenAId, tokenBId, tokenAAmount, tokenBAmount);
Expand All @@ -380,15 +426,15 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenAId: TokenId,
tokenBId: TokenId,
tokenAAmount: Balance,
tokenBAmountLimit: Balance
tokenBAmountLimit: Balance,
) {
const provider = this.transaction.sender.value;
this.addLiquidity(
provider,
tokenAId,
tokenBId,
tokenAAmount,
tokenBAmountLimit
tokenBAmountLimit,
);
}

Expand All @@ -398,7 +444,7 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenBId: TokenId,
lpTokenAmount: Balance,
tokenAAmountLimit: Balance,
tokenBLAmountLimit: Balance
tokenBLAmountLimit: Balance,
) {
const provider = this.transaction.sender.value;
this.removeLiquidity(
Expand All @@ -407,21 +453,21 @@ export class XYK extends RuntimeModule<XYKConfig> {
tokenBId,
lpTokenAmount,
tokenAAmountLimit,
tokenBLAmountLimit
tokenBLAmountLimit,
);
}

@runtimeMethod()
public sellPathSigned(
path: TokenIdPath,
amountIn: Balance,
amountOutMinLimit: Balance
amountOutMinLimit: Balance,
) {
this.sellPath(
this.transaction.sender.value,
path,
amountIn,
amountOutMinLimit
amountOutMinLimit,
);
}
}
Loading