Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
ba54cb9
nit
chrismaree Jan 28, 2022
84aad83
nit
chrismaree Jan 28, 2022
e971ff7
nit
chrismaree Jan 28, 2022
7a24fba
Update contracts/HubPool.sol
chrismaree Jan 31, 2022
2f99c84
review nit
chrismaree Jan 31, 2022
dcb052a
review nit
chrismaree Jan 31, 2022
487e7ea
feat(hubpool): Refactor hub pool to use 1D bitmap integer
chrismaree Jan 31, 2022
39ea052
nit
chrismaree Jan 31, 2022
febc4a6
WIP
chrismaree Feb 1, 2022
1ba6779
review nit
chrismaree Feb 1, 2022
1886cfd
Merge branch 'master' into chrismaree/third-pass-replayment-claim
chrismaree Feb 1, 2022
439138b
review nit
chrismaree Feb 1, 2022
b7e3ae2
nit
chrismaree Feb 1, 2022
1f6fc17
nit
chrismaree Feb 1, 2022
f64bf85
nit
chrismaree Feb 1, 2022
8bdcec7
nit
chrismaree Feb 1, 2022
89e642a
nit
chrismaree Feb 1, 2022
d6bc9bd
nit
chrismaree Feb 1, 2022
8de1b8f
nit
chrismaree Feb 1, 2022
98b2b52
nit
chrismaree Feb 2, 2022
eefd0c0
WIP
chrismaree Feb 2, 2022
5de87a2
WIP
chrismaree Feb 2, 2022
9d75d36
nit
chrismaree Feb 2, 2022
aca98e0
nit
chrismaree Feb 3, 2022
6307c53
Merge branch 'master' into chrismaree/fourth-pass-repayment-claim
chrismaree Feb 3, 2022
6d87b1d
nit
chrismaree Feb 3, 2022
b03f37e
nit
chrismaree Feb 3, 2022
f45389b
nit
chrismaree Feb 3, 2022
b33a77b
nit
chrismaree Feb 3, 2022
12d0811
nit
chrismaree Feb 3, 2022
b02f2a8
nit
chrismaree Feb 3, 2022
03c4d00
nit
chrismaree Feb 3, 2022
b6cca33
nit
chrismaree Feb 4, 2022
d4d27fc
nit
chrismaree Feb 4, 2022
1480614
WIP
chrismaree Feb 4, 2022
804bb41
nit
chrismaree Feb 4, 2022
ce3f29c
nit
chrismaree Feb 4, 2022
4d2e566
WIP
chrismaree Feb 4, 2022
3736565
nit
chrismaree Feb 4, 2022
bea5162
nit
chrismaree Feb 4, 2022
8c1278b
nit
chrismaree Feb 4, 2022
da8b83d
nit
chrismaree Feb 4, 2022
96cc1a4
nit
chrismaree Feb 4, 2022
66a75f2
nit
chrismaree Feb 4, 2022
e0c3307
nit
chrismaree Feb 4, 2022
ad372f6
nit
chrismaree Feb 4, 2022
4c986c4
nit
chrismaree Feb 5, 2022
ff1f10c
nit
chrismaree Feb 5, 2022
d5ce758
nit
chrismaree Feb 8, 2022
27d4371
nit
chrismaree Feb 8, 2022
2c98530
nit
chrismaree Feb 8, 2022
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
256 changes: 160 additions & 96 deletions contracts/HubPool.sol

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions contracts/LpTokenFactory.sol
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 {
Copy link
Member Author

@chrismaree chrismaree Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytecode saving.

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
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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));
}
}
1 change: 1 addition & 0 deletions contracts/MerkleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ library MerkleLib {
int256[] netSendAmounts;
// This is only here to be emitted in an event to track a running unpaid balance between the L2 pool and the L1 pool.
// A positive number indicates that the HubPool owes the SpokePool funds. A negative number indicates that the

// SpokePool owes the HubPool funds. See the comment above for the dynamics of this and netSendAmounts
int256[] runningBalances;
}
Expand Down
8 changes: 8 additions & 0 deletions contracts/interfaces/LpTokenFactoryInterface.sol
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);
}
23 changes: 13 additions & 10 deletions test/HubPool.Admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,34 @@ describe("HubPool Admin functions", function () {
});

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

const lpTokenStruct = await hubPool.callStatic.lpTokens(weth.address);
expect(lpTokenStruct.lpToken).to.not.equal(ZERO_ADDRESS);
expect(lpTokenStruct.isEnabled).to.equal(true);
const pooledTokenStruct = await hubPool.callStatic.pooledTokens(weth.address);
expect(pooledTokenStruct.lpToken).to.not.equal(ZERO_ADDRESS);
expect(pooledTokenStruct.isEnabled).to.equal(true);
expect(pooledTokenStruct.isWeth).to.equal(true);
expect(pooledTokenStruct.lastLpFeeUpdate).to.equal(Number(await hubPool.getCurrentTime()));

const lpToken = await (await getContractFactory("ExpandedERC20", owner)).attach(lpTokenStruct.lpToken);
const lpToken = await (await getContractFactory("ExpandedERC20", owner)).attach(pooledTokenStruct.lpToken);
expect(await lpToken.callStatic.symbol()).to.equal("Av2-WETH-LP");
expect(await lpToken.callStatic.name()).to.equal("Across Wrapped Ether LP Token");
});
it("Only owner can enable L1 Tokens for liquidity provision", async function () {
await expect(hubPool.connect(other).enableL1TokenForLiquidityProvision(weth.address)).to.be.reverted;
await expect(hubPool.connect(other).enableL1TokenForLiquidityProvision(weth.address, true)).to.be.reverted;
});
it("Can disable L1 Tokens for liquidity provision", async function () {
await hubPool.disableL1TokenForLiquidityProvision(weth.address);
expect((await hubPool.callStatic.lpTokens(weth.address)).isEnabled).to.equal(false);
expect((await hubPool.callStatic.pooledTokens(weth.address)).isEnabled).to.equal(false);
});
it("Only owner can disable L1 Tokens for liquidity provision", async function () {
await expect(hubPool.connect(other).disableL1TokenForLiquidityProvision(weth.address)).to.be.reverted;
});
it("Can whitelist route for deposits and rebalances", async function () {
await expect(hubPool.whitelistRoute(weth.address, usdc.address, destinationChainId))
await expect(hubPool.whitelistRoute(destinationChainId, weth.address, usdc.address))
.to.emit(hubPool, "WhitelistRoute")
.withArgs(weth.address, destinationChainId, usdc.address);
.withArgs(destinationChainId, weth.address, usdc.address);

expect(await hubPool.whitelistedRoutes(weth.address, destinationChainId)).to.equal(usdc.address);
});

Expand Down
108 changes: 108 additions & 0 deletions test/HubPool.Fees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { expect } from "chai";
Copy link
Member Author

@chrismaree chrismaree Feb 4, 2022

Choose a reason for hiding this comment

The 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));
});
});
27 changes: 12 additions & 15 deletions test/HubPool.Fixture.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TokenRolesEnum, interfaceName } from "@uma/common";
import { getContractFactory, randomAddress, toBN, fromWei } from "./utils";

import { bondAmount, refundProposalLiveness, finalFee, identifier, repaymentChainId } from "./constants";
import { Contract, Signer } from "ethers";
import hre from "hardhat";
Expand Down Expand Up @@ -32,17 +33,12 @@ export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) =

// Deploy the hubPool.
const merkleLib = await (await getContractFactory("MerkleLib", signer)).deploy();
const lpTokenFactory = await (await getContractFactory("LpTokenFactory", signer)).deploy();
const hubPool = await (
await getContractFactory("HubPool", { signer: signer, libraries: { MerkleLib: merkleLib.address } })
).deploy(
bondAmount,
refundProposalLiveness,
parentFixtureOutput.finder.address,
identifier,
weth.address,
weth.address,
parentFixtureOutput.timer.address
);
).deploy(lpTokenFactory.address, parentFixtureOutput.finder.address, parentFixtureOutput.timer.address);
await hubPool.setBond(weth.address, bondAmount);
await hubPool.setRefundProposalLiveness(refundProposalLiveness);

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

await hubPool.whitelistRoute(repaymentChainId, weth.address, l2Weth);
await hubPool.whitelistRoute(repaymentChainId, dai.address, l2Dai);
await hubPool.whitelistRoute(repaymentChainId, usdc.address, l2Usdc);

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

export async function enableTokensForLiquidityProvision(owner: Signer, hubPool: Contract, tokens: Contract[]) {
export async function enableTokensForLP(owner: Signer, hubPool: Contract, weth: Contract, tokens: Contract[]) {
const lpTokens = [];
for (const token of tokens) {
await hubPool.enableL1TokenForLiquidityProvision(token.address);
await hubPool.enableL1TokenForLiquidityProvision(token.address, token.address == weth.address);
lpTokens.push(
await (
await getContractFactory("ExpandedERC20", owner)
).attach((await hubPool.callStatic.lpTokens(token.address)).lpToken)
).attach((await hubPool.callStatic.pooledTokens(token.address)).lpToken)
);
}
return lpTokens;
Expand Down
6 changes: 3 additions & 3 deletions test/HubPool.LiquidityProvision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from "chai";
import { Contract } from "ethers";
import { ethers } from "hardhat";
import { getContractFactory, fromWei, toBN, SignerWithAddress, seedWallet } from "./utils";
import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture";
import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture";
import { amountToSeedWallets, amountToLp } from "./constants";

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

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

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