Skip to content

Commit 5b71325

Browse files
chrismareemrice32
andauthored
feat: Initial fees tracking implementation (#21)
* nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * Update contracts/HubPool.sol Co-authored-by: Matt Rice <matthewcrice32@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * feat(hubpool): Refactor hub pool to use 1D bitmap integer Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP Signed-off-by: chrismaree <christopher.maree@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> Co-authored-by: Matt Rice <matthewcrice32@gmail.com>
1 parent c6a59c1 commit 5b71325

File tree

11 files changed

+372
-138
lines changed

11 files changed

+372
-138
lines changed

contracts/HubPool.sol

Lines changed: 160 additions & 95 deletions
Large diffs are not rendered by default.

contracts/LpTokenFactory.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: GPL-3.0-only
2+
pragma solidity ^0.8.0;
3+
4+
import "./interfaces/LpTokenFactoryInterface.sol";
5+
6+
import "@uma/core/contracts/common/implementation/ExpandedERC20.sol";
7+
8+
contract LpTokenFactory is LpTokenFactoryInterface {
9+
function createLpToken(address l1Token) public returns (address) {
10+
ExpandedERC20 lpToken = new ExpandedERC20(
11+
_append("Across ", IERC20Metadata(l1Token).name(), " LP Token"), // LP Token Name
12+
_append("Av2-", IERC20Metadata(l1Token).symbol(), "-LP"), // LP Token Symbol
13+
IERC20Metadata(l1Token).decimals() // LP Token Decimals
14+
);
15+
lpToken.addMember(1, msg.sender); // Set this contract as the LP Token's minter.
16+
lpToken.addMember(2, msg.sender); // Set this contract as the LP Token's burner.
17+
18+
return address(lpToken);
19+
}
20+
21+
function _append(
22+
string memory a,
23+
string memory b,
24+
string memory c
25+
) internal pure returns (string memory) {
26+
return string(abi.encodePacked(a, b, c));
27+
}
28+
}

contracts/MerkleLib.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ library MerkleLib {
2929
int256[] netSendAmounts;
3030
// This is only here to be emitted in an event to track a running unpaid balance between the L2 pool and the L1 pool.
3131
// A positive number indicates that the HubPool owes the SpokePool funds. A negative number indicates that the
32+
3233
// SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmounts
3334
int256[] runningBalances;
3435
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: AGPL-3.0-only
2+
pragma solidity ^0.8.0;
3+
4+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
6+
interface LpTokenFactoryInterface {
7+
function createLpToken(address l1Token) external returns (address);
8+
}

test/HubPool.Admin.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,34 @@ describe("HubPool Admin functions", function () {
1616
});
1717

1818
it("Can add L1 token to whitelisted lpTokens mapping", async function () {
19-
expect((await hubPool.callStatic.lpTokens(weth.address)).lpToken).to.equal(ZERO_ADDRESS);
20-
await hubPool.enableL1TokenForLiquidityProvision(weth.address);
19+
expect((await hubPool.callStatic.pooledTokens(weth.address)).lpToken).to.equal(ZERO_ADDRESS);
20+
await hubPool.enableL1TokenForLiquidityProvision(weth.address, true);
2121

22-
const lpTokenStruct = await hubPool.callStatic.lpTokens(weth.address);
23-
expect(lpTokenStruct.lpToken).to.not.equal(ZERO_ADDRESS);
24-
expect(lpTokenStruct.isEnabled).to.equal(true);
22+
const pooledTokenStruct = await hubPool.callStatic.pooledTokens(weth.address);
23+
expect(pooledTokenStruct.lpToken).to.not.equal(ZERO_ADDRESS);
24+
expect(pooledTokenStruct.isEnabled).to.equal(true);
25+
expect(pooledTokenStruct.isWeth).to.equal(true);
26+
expect(pooledTokenStruct.lastLpFeeUpdate).to.equal(Number(await hubPool.getCurrentTime()));
2527

26-
const lpToken = await (await getContractFactory("ExpandedERC20", owner)).attach(lpTokenStruct.lpToken);
28+
const lpToken = await (await getContractFactory("ExpandedERC20", owner)).attach(pooledTokenStruct.lpToken);
2729
expect(await lpToken.callStatic.symbol()).to.equal("Av2-WETH-LP");
2830
expect(await lpToken.callStatic.name()).to.equal("Across Wrapped Ether LP Token");
2931
});
3032
it("Only owner can enable L1 Tokens for liquidity provision", async function () {
31-
await expect(hubPool.connect(other).enableL1TokenForLiquidityProvision(weth.address)).to.be.reverted;
33+
await expect(hubPool.connect(other).enableL1TokenForLiquidityProvision(weth.address, true)).to.be.reverted;
3234
});
3335
it("Can disable L1 Tokens for liquidity provision", async function () {
3436
await hubPool.disableL1TokenForLiquidityProvision(weth.address);
35-
expect((await hubPool.callStatic.lpTokens(weth.address)).isEnabled).to.equal(false);
37+
expect((await hubPool.callStatic.pooledTokens(weth.address)).isEnabled).to.equal(false);
3638
});
3739
it("Only owner can disable L1 Tokens for liquidity provision", async function () {
3840
await expect(hubPool.connect(other).disableL1TokenForLiquidityProvision(weth.address)).to.be.reverted;
3941
});
4042
it("Can whitelist route for deposits and rebalances", async function () {
41-
await expect(hubPool.whitelistRoute(weth.address, usdc.address, destinationChainId))
43+
await expect(hubPool.whitelistRoute(destinationChainId, weth.address, usdc.address))
4244
.to.emit(hubPool, "WhitelistRoute")
43-
.withArgs(weth.address, destinationChainId, usdc.address);
45+
.withArgs(destinationChainId, weth.address, usdc.address);
46+
4447
expect(await hubPool.whitelistedRoutes(weth.address, destinationChainId)).to.equal(usdc.address);
4548
});
4649

test/HubPool.Fees.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { expect } from "chai";
2+
import { Contract } from "ethers";
3+
import { ethers } from "hardhat";
4+
import { SignerWithAddress, toBNWei, seedWallet, toWei } from "./utils";
5+
import * as consts from "./constants";
6+
import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture";
7+
import { buildPoolRebalanceTree, buildPoolRebalanceLeafs } from "./MerkleLib.utils";
8+
9+
let hubPool: Contract, weth: Contract, timer: Contract;
10+
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;
11+
12+
async function constructSimpleTree() {
13+
const wethSendToL2 = toBNWei(100);
14+
const wethAttributeToLps = toBNWei(10);
15+
const leafs = buildPoolRebalanceLeafs(
16+
[consts.repaymentChainId], // repayment chain. In this test we only want to send one token to one chain.
17+
[weth], // l1Token. We will only be sending WETH and DAI to the associated repayment chain.
18+
[[wethAttributeToLps]], // bundleLpFees. Set to 1 ETH and 10 DAI respectively to attribute to the LPs.
19+
[[wethSendToL2]], // netSendAmounts. Set to 100 ETH and 1000 DAI as the amount to send from L1->L2.
20+
[[wethSendToL2]] // runningBalances. Set to 100 ETH and 1000 DAI.
21+
);
22+
const tree = await buildPoolRebalanceTree(leafs);
23+
24+
return { wethSendToL2, wethAttributeToLps, leafs, tree };
25+
}
26+
27+
describe("HubPool LP fees", function () {
28+
beforeEach(async function () {
29+
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
30+
({ weth, hubPool, timer } = await hubPoolFixture());
31+
await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2));
32+
await seedWallet(liquidityProvider, [], weth, consts.amountToLp.mul(10));
33+
34+
await enableTokensForLP(owner, hubPool, weth, [weth]);
35+
await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp);
36+
await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp);
37+
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));
38+
});
39+
40+
it("Fee tracking variables are correctly updated at the execution of a refund", async function () {
41+
// Before any execution happens liquidity trackers are set as expected.
42+
const pooledTokenInfoPreExecution = await hubPool.pooledTokens(weth.address);
43+
expect(pooledTokenInfoPreExecution.liquidReserves).to.eq(consts.amountToLp);
44+
expect(pooledTokenInfoPreExecution.utilizedReserves).to.eq(0);
45+
expect(pooledTokenInfoPreExecution.undistributedLpFees).to.eq(0);
46+
expect(pooledTokenInfoPreExecution.lastLpFeeUpdate).to.eq(await timer.getCurrentTime());
47+
expect(pooledTokenInfoPreExecution.isWeth).to.eq(true);
48+
49+
const { wethSendToL2, wethAttributeToLps, leafs, tree } = await constructSimpleTree();
50+
51+
await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), consts.mockTreeRoot);
52+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness);
53+
await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0]));
54+
55+
// Validate the post execution values have updated as expected. Liquid reserves should be the original LPed amount
56+
// minus the amount sent to L2. Utilized reserves should be the amount sent to L2 plus the attribute to LPs.
57+
// Undistributed LP fees should be attribute to LPs.
58+
const pooledTokenInfoPostExecution = await hubPool.pooledTokens(weth.address);
59+
expect(pooledTokenInfoPostExecution.liquidReserves).to.eq(consts.amountToLp.sub(wethSendToL2));
60+
expect(pooledTokenInfoPostExecution.utilizedReserves).to.eq(wethSendToL2.add(wethAttributeToLps));
61+
expect(pooledTokenInfoPostExecution.undistributedLpFees).to.eq(wethAttributeToLps);
62+
});
63+
64+
it("Exchange rate current correctly attributes fees over the smear period", async function () {
65+
// Fees are designed to be attributed over a period of time so they dont all arrive on L1 as soon as the bundle is
66+
// executed. We can validate that fees are correctly smeared by attributing some and then moving time forward and
67+
// validating that key variable shift as a function of time.
68+
const { leafs, tree } = await constructSimpleTree();
69+
70+
// Exchange rate current before any fees are attributed execution should be 1.
71+
expect(await hubPool.callStatic.exchangeRateCurrent(weth.address)).to.eq(toWei(1));
72+
await hubPool.exchangeRateCurrent(weth.address);
73+
74+
await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), consts.mockTreeRoot);
75+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness);
76+
await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0]));
77+
78+
// Exchange rate current right after the refund execution should be the amount deposited, grown by the 100 second
79+
// liveness period. Of the 10 ETH attributed to LPs, a total of 10*0.0000015*100=0.0015 was attributed to LPs.
80+
// The exchange rate is therefore (1000+0.0015)/1000=1.0000015.
81+
expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.0000015));
82+
83+
// Validate the state variables are updated accordingly. In particular, undistributedLpFees should have decremented
84+
// by the amount allocated in the previous computation. This should be 10-0.0015=9.9985.
85+
await hubPool.exchangeRateCurrent(weth.address); // force state sync.
86+
expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(9.9985));
87+
88+
// Next, advance time 2 days. Compute the ETH attributed to LPs by multiplying the original amount allocated(10),
89+
// minus the previous computation amount(0.0015) by the smear rate, by the duration to get the second periods
90+
// allocation of(10 - 0.0015) * 0.0000015 * (172800)=2.5916112.The exchange rate should be The sum of the
91+
// liquidity provided and the fees added in both periods as (1000+0.0015+2.5916112)/1000=1.0025931112.
92+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 2 * 24 * 60 * 60);
93+
expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.0025931112));
94+
95+
// Again, we can validate that the undistributedLpFees have been updated accordingly. This should be set to the
96+
// original amount (10) minus the two sets of attributed LP fees as 10-0.0015-2.5916112=7.4068888.
97+
await hubPool.exchangeRateCurrent(weth.address); // force state sync.
98+
expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(7.4068888));
99+
100+
// Finally, advance time past the end of the smear period by moving forward 10 days. At this point all LP fees
101+
// should be attributed such that undistributedLpFees=0 and the exchange rate should simply be (1000+10)/1000=1.01.
102+
await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 10 * 24 * 60 * 60);
103+
expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.01));
104+
await hubPool.exchangeRateCurrent(weth.address); // force state sync.
105+
expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(0));
106+
});
107+
});

test/HubPool.Fixture.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TokenRolesEnum, interfaceName } from "@uma/common";
22
import { getContractFactory, randomAddress, toBN, fromWei } from "./utils";
3+
34
import { bondAmount, refundProposalLiveness, finalFee, identifier, repaymentChainId } from "./constants";
45
import { Contract, Signer } from "ethers";
56
import hre from "hardhat";
@@ -32,17 +33,12 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) =
3233

3334
// Deploy the hubPool.
3435
const merkleLib = await (await getContractFactory("MerkleLib", signer)).deploy();
36+
const lpTokenFactory = await (await getContractFactory("LpTokenFactory", signer)).deploy();
3537
const hubPool = await (
3638
await getContractFactory("HubPool", { signer: signer, libraries: { MerkleLib: merkleLib.address } })
37-
).deploy(
38-
bondAmount,
39-
refundProposalLiveness,
40-
parentFixtureOutput.finder.address,
41-
identifier,
42-
weth.address,
43-
weth.address,
44-
parentFixtureOutput.timer.address
45-
);
39+
).deploy(lpTokenFactory.address, parentFixtureOutput.finder.address, parentFixtureOutput.timer.address);
40+
await hubPool.setBond(weth.address, bondAmount);
41+
await hubPool.setRefundProposalLiveness(refundProposalLiveness);
4642

4743
// Deploy a mock chain adapter and add it as the chainAdapter for the test chainId. Set the SpokePool to address 0.
4844
const mockAdapter = await (await getContractFactory("Mock_Adapter", signer)).deploy();
@@ -56,21 +52,22 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) =
5652
const l2Weth = randomAddress();
5753
const l2Dai = randomAddress();
5854
const l2Usdc = randomAddress();
59-
await hubPool.whitelistRoute(weth.address, l2Weth, repaymentChainId);
60-
await hubPool.whitelistRoute(dai.address, l2Dai, repaymentChainId);
61-
await hubPool.whitelistRoute(usdc.address, l2Usdc, repaymentChainId);
55+
56+
await hubPool.whitelistRoute(repaymentChainId, weth.address, l2Weth);
57+
await hubPool.whitelistRoute(repaymentChainId, dai.address, l2Dai);
58+
await hubPool.whitelistRoute(repaymentChainId, usdc.address, l2Usdc);
6259

6360
return { weth, usdc, dai, hubPool, mockAdapter, mockSpoke, l2Weth, l2Dai, l2Usdc, ...parentFixtureOutput };
6461
});
6562

66-
export async function enableTokensForLiquidityProvision(owner: Signer, hubPool: Contract, tokens: Contract[]) {
63+
export async function enableTokensForLP(owner: Signer, hubPool: Contract, weth: Contract, tokens: Contract[]) {
6764
const lpTokens = [];
6865
for (const token of tokens) {
69-
await hubPool.enableL1TokenForLiquidityProvision(token.address);
66+
await hubPool.enableL1TokenForLiquidityProvision(token.address, token.address == weth.address);
7067
lpTokens.push(
7168
await (
7269
await getContractFactory("ExpandedERC20", owner)
73-
).attach((await hubPool.callStatic.lpTokens(token.address)).lpToken)
70+
).attach((await hubPool.callStatic.pooledTokens(token.address)).lpToken)
7471
);
7572
}
7673
return lpTokens;

test/HubPool.LiquidityProvision.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect } from "chai";
22
import { Contract } from "ethers";
33
import { ethers } from "hardhat";
44
import { getContractFactory, fromWei, toBN, SignerWithAddress, seedWallet } from "./utils";
5-
import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture";
5+
import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture";
66
import { amountToSeedWallets, amountToLp } from "./constants";
77

88
let hubPool: Contract, weth: Contract, usdc: Contract, dai: Contract;
@@ -13,7 +13,7 @@ describe("HubPool Liquidity Provision", function () {
1313
beforeEach(async function () {
1414
[owner, liquidityProvider, other] = await ethers.getSigners();
1515
({ weth, usdc, dai, hubPool } = await hubPoolFixture());
16-
[wethLpToken, usdcLpToken, daiLpToken] = await enableTokensForLiquidityProvision(owner, hubPool, [weth, usdc, dai]);
16+
[wethLpToken, usdcLpToken, daiLpToken] = await enableTokensForLP(owner, hubPool, weth, [weth, usdc, dai]);
1717

1818
// mint some fresh tokens and deposit ETH for weth for the liquidity provider.
1919
await seedWallet(liquidityProvider, [usdc, dai], weth, amountToSeedWallets);
@@ -22,7 +22,7 @@ describe("HubPool Liquidity Provision", function () {
2222
it("Adding ER20 liquidity correctly pulls tokens and mints LP tokens", async function () {
2323
const daiLpToken = await (
2424
await getContractFactory("ExpandedERC20", owner)
25-
).attach((await hubPool.callStatic.lpTokens(dai.address)).lpToken);
25+
).attach((await hubPool.callStatic.pooledTokens(dai.address)).lpToken);
2626

2727
// Balances of collateral before should equal the seed amount and there should be 0 outstanding LP tokens.
2828
expect(await dai.balanceOf(liquidityProvider.address)).to.equal(amountToSeedWallets);

0 commit comments

Comments
 (0)