-
Notifications
You must be signed in to change notification settings - Fork 78
feat: Initial fees tracking implementation #21
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
Changes from 42 commits
ba54cb9
84aad83
e971ff7
7a24fba
2f99c84
dcb052a
487e7ea
39ea052
febc4a6
1ba6779
1886cfd
439138b
b7e3ae2
1f6fc17
f64bf85
8bdcec7
89e642a
d6bc9bd
8de1b8f
98b2b52
eefd0c0
5de87a2
9d75d36
aca98e0
6307c53
6d87b1d
b03f37e
f45389b
b33a77b
12d0811
b02f2a8
03c4d00
b6cca33
d4d27fc
1480614
804bb41
ce3f29c
4d2e566
3736565
bea5162
8c1278b
da8b83d
96cc1a4
66a75f2
e0c3307
ad372f6
4c986c4
ff1f10c
d5ce758
27d4371
2c98530
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| // SPDX-License-Identifier: GPL-3.0-only | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "./interfaces/LpTokenFactoryInterface.sol"; | ||
|
|
||
| import "@uma/core/contracts/common/implementation/ExpandedERC20.sol"; | ||
|
|
||
| contract LpTokenFactory is LpTokenFactoryInterface { | ||
| function createLpToken(address l1Token) public returns (address) { | ||
| ExpandedERC20 lpToken = new ExpandedERC20( | ||
| _append("Across ", IERC20Metadata(l1Token).name(), " LP Token"), // LP Token Name | ||
| _append("Av2-", IERC20Metadata(l1Token).symbol(), "-LP"), // LP Token Symbol | ||
|
Comment on lines
+11
to
+12
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like that all these string constants, etc can sit here. As a side note, we should run these names by the rest of the team.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yup! saves a fair bit. for now I think it's fine. I dont think it's super important, as long as it's diffrent to the other tokens. |
||
| IERC20Metadata(l1Token).decimals() // LP Token Decimals | ||
| ); | ||
| lpToken.addMember(1, msg.sender); // Set this contract as the LP Token's minter. | ||
| lpToken.addMember(2, msg.sender); // Set this contract as the LP Token's burner. | ||
|
|
||
| return address(lpToken); | ||
| } | ||
|
|
||
| function _append( | ||
| string memory a, | ||
| string memory b, | ||
| string memory c | ||
| ) internal pure returns (string memory) { | ||
| return string(abi.encodePacked(a, b, c)); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| // SPDX-License-Identifier: AGPL-3.0-only | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; | ||
|
|
||
| interface LpTokenFactoryInterface { | ||
| function createLpToken(address l1Token) external returns (address); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| import { expect } from "chai"; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please read through this test file! the second test covers a full commented evaluation of fees over a smear period. |
||
| import { Contract } from "ethers"; | ||
| import { ethers } from "hardhat"; | ||
|
|
||
| import { SignerWithAddress, toBNWei, seedWallet, toWei } from "./utils"; | ||
| import * as consts from "./constants"; | ||
| import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture"; | ||
| import { buildPoolRebalanceTree, buildPoolRebalanceLeafs } from "./MerkleLib.utils"; | ||
|
|
||
| let hubPool: Contract, mockAdapter: Contract, weth: Contract, mockSpoke: Contract, timer: Contract; | ||
| let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; | ||
|
|
||
| async function constructSimpleTree() { | ||
| const wethSendToL2 = toBNWei(100); | ||
| const wethAttributeToLps = toBNWei(10); | ||
| const leafs = buildPoolRebalanceLeafs( | ||
| [consts.repaymentChainId], // repayment chain. In this test we only want to send one token to one chain. | ||
| [weth], // l1Token. We will only be sending WETH and DAI to the associated repayment chain. | ||
| [[wethAttributeToLps]], // bundleLpFees. Set to 1 ETH and 10 DAI respectively to attribute to the LPs. | ||
| [[wethSendToL2]], // netSendAmounts. Set to 100 ETH and 1000 DAI as the amount to send from L1->L2. | ||
| [[wethSendToL2]] // runningBalances. Set to 100 ETH and 1000 DAI. | ||
| ); | ||
| const tree = await buildPoolRebalanceTree(leafs); | ||
|
|
||
| return { wethSendToL2, wethAttributeToLps, leafs, tree }; | ||
| } | ||
|
|
||
| describe.only("HubPool LP fees", function () { | ||
| beforeEach(async function () { | ||
| [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); | ||
| ({ weth, hubPool, mockAdapter, mockSpoke, timer } = await hubPoolFixture()); | ||
| await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2)); | ||
| await seedWallet(liquidityProvider, [], weth, consts.amountToLp.mul(10)); | ||
|
|
||
| await enableTokensForLP(owner, hubPool, weth, [weth]); | ||
| await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp); | ||
| await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10)); | ||
| }); | ||
|
|
||
| it("Fee tracking variables are correctly updated at the execution of a refund", async function () { | ||
| // Before any execution happens liquidity trackers are set as expected. | ||
| const pooledTokenInfoPreExecution = await hubPool.pooledTokens(weth.address); | ||
| expect(pooledTokenInfoPreExecution.liquidReserves).to.eq(consts.amountToLp); | ||
| expect(pooledTokenInfoPreExecution.utilizedReserves).to.eq(0); | ||
| expect(pooledTokenInfoPreExecution.undistributedLpFees).to.eq(0); | ||
| expect(pooledTokenInfoPreExecution.lastLpFeeUpdate).to.eq(await timer.getCurrentTime()); | ||
| expect(pooledTokenInfoPreExecution.isWeth).to.eq(true); | ||
|
|
||
| const { wethSendToL2, wethAttributeToLps, leafs, tree } = await constructSimpleTree(); | ||
|
|
||
| await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), consts.mockTreeRoot); | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); | ||
| await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); | ||
|
|
||
| // Validate the post execution values have updated as expected. Liquid reserves should be the original LPed amount | ||
| // minus the amount sent to L2. Utilized reserves should be the amount sent to L2 plus the attribute to LPs. | ||
| // Undistributed LP fees should be attribute to LPs. | ||
| const pooledTokenInfoPostExecution = await hubPool.pooledTokens(weth.address); | ||
| expect(pooledTokenInfoPostExecution.liquidReserves).to.eq(consts.amountToLp.sub(wethSendToL2)); | ||
| expect(pooledTokenInfoPostExecution.utilizedReserves).to.eq(wethSendToL2.add(wethAttributeToLps)); | ||
| expect(pooledTokenInfoPostExecution.undistributedLpFees).to.eq(wethAttributeToLps); | ||
| }); | ||
|
|
||
| it("Exchange rate current correctly attributes fees over the smear period", async function () { | ||
| // Fees are designed to be attributed over a period of time so they dont all arrive on L1 as soon as the bundle is | ||
| // executed. We can validate that fees are correctly smeared by attributing some and then moving time forward and | ||
| // validating that key variable shift as a function of time. | ||
| const { leafs, tree } = await constructSimpleTree(); | ||
|
|
||
| // Exchange rate current before any fees are attributed execution should be 1. | ||
| expect(await hubPool.callStatic.exchangeRateCurrent(weth.address)).to.eq(toWei(1)); | ||
| await hubPool.exchangeRateCurrent(weth.address); | ||
|
|
||
| await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), consts.mockTreeRoot); | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness); | ||
| await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); | ||
|
|
||
| // Exchange rate current right after the refund execution should be the amount deposited, grown by the 100 second | ||
| // liveness period. Of the 10 ETH attributed to LPs, a total of 10*0.0000015*100=0.0015 was attributed to LPs. | ||
| // The exchange rate is therefore (1000+0.0015)/1000=1.0000015. | ||
| expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.0000015)); | ||
|
|
||
| // Validate the state variables are updated accordingly. In particular, undistributedLpFees should have decremented | ||
| // by the amount allocated in the previous computation. This should be 10-0.0015=9.9985. | ||
| await hubPool.exchangeRateCurrent(weth.address); // force state sync. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(9.9985)); | ||
|
|
||
| // Next, advance time 2 days. Compute the ETH attributed to LPs by multiplying the original amount allocated(10), | ||
| // minus the previous computation amount(0.0015) by the smear rate, by the duration to get the second periods | ||
| // allocation of(10 - 0.0015) * 0.0000015 * (172800)=2.5916112.The exchange rate should be The sum of the | ||
| // liquidity provided and the fees added in both periods as (1000+0.0015+2.5916112)/1000=1.0025931112. | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 2 * 24 * 60 * 60); | ||
| expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.0025931112)); | ||
|
|
||
| // Again, we can validate that the undistributedLpFees have been updated accordingly. This should be set to the | ||
| // original amount (10) minus the two sets of attributed LP fees as 10-0.0015-2.5916112=7.4068888. | ||
| await hubPool.exchangeRateCurrent(weth.address); // force state sync. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(7.4068888)); | ||
|
|
||
| // Finally, advance time past the end of the smear period by moving forward 10 days. At this point all LP fees | ||
| // should be attributed such that undistributedLpFees=0 and the exchange rate should simply be (1000+10)/1000=1.01. | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 10 * 24 * 60 * 60); | ||
| expect((await hubPool.callStatic.exchangeRateCurrent(weth.address)).toString()).to.eq(toWei(1.01)); | ||
| await hubPool.exchangeRateCurrent(weth.address); // force state sync. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.eq(toWei(0)); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bytecode saving.