Skip to content

Commit

Permalink
Merge pull request #35 from balancer/26-add-example-hook
Browse files Browse the repository at this point in the history
26 add example hook
  • Loading branch information
johngrantuk authored Jul 30, 2024
2 parents c89e3fe + 933ffbb commit a89dc4c
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 111 deletions.
94 changes: 94 additions & 0 deletions typescript/src/hooks/exitFeeHook.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,97 @@
import { RemoveKind } from '../vault/types';
import { HookBase } from './types';
import { MathSol } from '../utils/math';

export type HookStateExitFee = {
tokens: string[];
removeLiquidityHookFeePercentage: bigint;
};

/**
* This hook implements the ExitFeeHookExample found in mono-repo: https://github.com/balancer/balancer-v3-monorepo/blob/c848c849cb44dc35f05d15858e4fba9f17e92d5e/pkg/pool-hooks/contracts/ExitFeeHookExample.sol
*/
export class ExitFeeHook implements HookBase {
public shouldCallComputeDynamicSwapFee = false;
public shouldCallBeforeSwap = false;
public shouldCallAfterSwap = false;
public shouldCallBeforeAddLiquidity = false;
public shouldCallAfterAddLiquidity = false;
public shouldCallBeforeRemoveLiquidity = false;
public shouldCallAfterRemoveLiquidity = true;
public enableHookAdjustedAmounts = true;

onBeforeAddLiquidity() {
return { success: false, hookAdjustedBalancesScaled18: [] };
}
onAfterAddLiquidity() {
return { success: false, hookAdjustedAmountsInRaw: [] };
}
onBeforeRemoveLiquidity() {
return { success: false, hookAdjustedBalancesScaled18: [] };
}
onAfterRemoveLiquidity(
kind: RemoveKind,
bptAmountIn: bigint,
amountsOutScaled18: bigint[],
amountsOutRaw: bigint[],
balancesScaled18: bigint[],
hookState: HookStateExitFee,
) {
if (
!(
typeof hookState === 'object' &&
hookState !== null &&
'removeLiquidityHookFeePercentage' in hookState &&
'tokens' in hookState
)
)
throw new Error('Unexpected hookState');

// Our current architecture only supports fees on tokens. Since we must always respect exact `amountsOut`, and
// non-proportional remove liquidity operations would require taking fees in BPT, we only support proportional
// removeLiquidity.
if (kind !== RemoveKind.PROPORTIONAL) {
throw new Error(`ExitFeeHook: Unsupported RemoveKind: ${kind}`);
}
const accruedFees = new Array(hookState.tokens.length).fill(0n);
const hookAdjustedAmountsOutRaw = [...amountsOutRaw];
if (hookState.removeLiquidityHookFeePercentage > 0) {
// Charge fees proportional to amounts out of each token
for (let i = 0; i < amountsOutRaw.length; i++) {
const hookFee = MathSol.mulDownFixed(
amountsOutRaw[i],
hookState.removeLiquidityHookFeePercentage,
);
accruedFees[i] = hookFee;
hookAdjustedAmountsOutRaw[i] -= hookFee;
// Fees don't need to be transferred to the hook, because donation will reinsert them in the vault
}

// In SC Hook Donates accrued fees back to LPs
// _vault.addLiquidity(
// AddLiquidityParams({
// pool: pool,
// to: msg.sender, // It would mint BPTs to router, but it's a donation so no BPT is minted
// maxAmountsIn: accruedFees, // Donate all accrued fees back to the pool (i.e. to the LPs)
// minBptAmountOut: 0, // Donation does not return BPTs, any number above 0 will revert
// kind: AddLiquidityKind.DONATION,
// userData: bytes(''), // User data is not used by donation, so we can set to an empty string
// }),
// );
}

return {
success: true,
hookAdjustedAmountsOutRaw,
};
}
onBeforeSwap() {
return { success: false, hookAdjustedBalancesScaled18: [] };
}
onAfterSwap() {
return { success: false, hookAdjustedAmountCalculatedRaw: 0n };
}
onComputeDynamicSwapFee() {
return { success: false, dynamicSwapFee: 0n };
}
}
2 changes: 2 additions & 0 deletions typescript/src/vault/vault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from './types';
import { HookBase, HookClassConstructor, HookState } from '../hooks/types';
import { defaultHook } from '../hooks/constants';
import { ExitFeeHook } from '../hooks/exitFeeHook';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PoolClassConstructor = new (..._args: any[]) => PoolBase;
Expand All @@ -46,6 +47,7 @@ export class Vault {
...customPoolClasses,
};
this.hookClasses = {
ExitFee: ExitFeeHook,
// custom hooks take precedence over base types
...hookClasses,
};
Expand Down
7 changes: 7 additions & 0 deletions typescript/test/customPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ class CustomPool implements PoolBase {
return 1n;
}

getMaxSingleTokenRemoveAmount() {
return 1n;
}
getMaxSingleTokenAddAmount() {
return 1n;
}

onSwap(): bigint {
return this.randoms[0];
}
Expand Down
7 changes: 7 additions & 0 deletions typescript/test/hooks/afterRemoveLiquidity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ class CustomPool implements PoolBase {
return 1n;
}

getMaxSingleTokenRemoveAmount() {
return 1n;
}
getMaxSingleTokenAddAmount() {
return 1n;
}

onSwap(): bigint {
return 1n;
}
Expand Down
7 changes: 7 additions & 0 deletions typescript/test/hooks/afterSwap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ class CustomPool implements PoolBase {
return 1n;
}

getMaxSingleTokenRemoveAmount() {
return 1n;
}
getMaxSingleTokenAddAmount() {
return 1n;
}

onSwap(): bigint {
return 100000000000n;
}
Expand Down
7 changes: 7 additions & 0 deletions typescript/test/hooks/beforeRemoveLiquidity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ class CustomPool implements PoolBase {
return 1n;
}

getMaxSingleTokenRemoveAmount() {
return 1n;
}
getMaxSingleTokenAddAmount() {
return 1n;
}

onSwap(): bigint {
return 1n;
}
Expand Down
111 changes: 0 additions & 111 deletions typescript/test/hooks/dynamicFee.test.ts

This file was deleted.

65 changes: 65 additions & 0 deletions typescript/test/hooks/exitFee.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// pnpm test -- exitFee.test.ts
import { describe, expect, test } from 'vitest';
import { RemoveKind, Vault } from '../../src';

const poolState = {
poolType: 'Weighted',
hookType: 'ExitFee',
chainId: '11155111',
blockNumber: '5955145',
poolAddress: '0x03722034317d8fb16845213bd3ce15439f9ce136',
tokens: [
'0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9',
'0xb19382073c7A0aDdbb56Ac6AF1808Fa49e377B75',
],
scalingFactors: [1000000000000000000n, 1000000000000000000n],
weights: [500000000000000000n, 500000000000000000n],
swapFee: 100000000000000000n,
aggregateSwapFee: 0n,
balancesLiveScaled18: [5000000000000000n, 5000000000000000000n],
tokenRates: [1000000000000000000n, 1000000000000000000n],
totalSupply: 158113883008415798n,
};

const removeLiquidityInput = {
pool: '0xb2456a6f51530053bc41b0ee700fe6a2c37282e8',
minAmountsOut: [1n, 1n],
maxBptAmountIn: 10000000000000n,
kind: RemoveKind.PROPORTIONAL,
};

describe('hook - exitFee', () => {
const vault = new Vault();

test('exitFee of 0', () => {
const inputHookState = {
removeLiquidityHookFeePercentage: 0n,
tokens: poolState.tokens,
};
const outPutAmount = vault.removeLiquidity(
removeLiquidityInput,
poolState,
inputHookState,
);
expect(outPutAmount.amountsOut).to.deep.eq([
316227766016n,
316227766016840n,
]);
});

test('exitFee of 5%', () => {
const inputHookState = {
removeLiquidityHookFeePercentage: 50000000000000000n,
tokens: poolState.tokens,
};
const outPutAmount = vault.removeLiquidity(
removeLiquidityInput,
poolState,
inputHookState,
);
expect(outPutAmount.amountsOut).to.deep.eq([
300416377716n,
300416377715998n,
]);
});
});
7 changes: 7 additions & 0 deletions typescript/test/hooks/hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ class CustomPool implements PoolBase {
return 1n;
}

getMaxSingleTokenRemoveAmount() {
return 1n;
}
getMaxSingleTokenAddAmount() {
return 1n;
}

onSwap(): bigint {
return this.randoms[0];
}
Expand Down

0 comments on commit a89dc4c

Please sign in to comment.