Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 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
c46e94f
feat(slack-config): Add sync method and liquidity utilization
chrismaree Feb 8, 2022
b884d2f
nit
chrismaree Feb 8, 2022
505ee77
Merge branch 'master' into chrismaree/fee-tracking-2
chrismaree Feb 8, 2022
986df76
Delete settings.json
chrismaree Feb 8, 2022
2238088
nit
chrismaree Feb 8, 2022
5bd1473
nit
chrismaree Feb 8, 2022
408223c
nit
chrismaree Feb 8, 2022
24928d5
nit
chrismaree Feb 8, 2022
3d74944
nit
chrismaree Feb 8, 2022
08281ac
nit
chrismaree Feb 8, 2022
0a79da6
nit
chrismaree Feb 8, 2022
13b022b
nit
chrismaree Feb 8, 2022
edce0cc
nit
chrismaree Feb 8, 2022
3ded224
nit
chrismaree Feb 9, 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
80 changes: 64 additions & 16 deletions contracts/HubPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
struct PooledToken {
address lpToken;
bool isEnabled;
uint256 liquidReserves;
Copy link
Member Author

Choose a reason for hiding this comment

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

re-order for better packing.

bool isWeth;
uint32 lastLpFeeUpdate;
int256 utilizedReserves;
uint256 liquidReserves;
uint256 undistributedLpFees;
uint32 lastLpFeeUpdate;
bool isWeth;
}

mapping(address => PooledToken) public pooledTokens;
Expand Down Expand Up @@ -186,13 +186,8 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
address originToken,
address destinationToken
) public onlyOwner {
//TODO In the future this should call cross-chain adapters to setEnableRoute.
whitelistedRoutes[originToken][destinationChainId] = destinationToken;

emit WhitelistRoute(destinationChainId, originToken, destinationToken);

// TODO: Should relay message to L2 for destinationChainId and call setEnableRoute(originToken, destinationChainId, true)
Copy link
Member

Choose a reason for hiding this comment

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

why remove comment? is this TODO resolved?

Copy link
Member Author

Choose a reason for hiding this comment

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

mistake in merging. added back.


whitelistedRoutes[originToken][destinationChainId] = destinationToken;
emit WhitelistRoute(destinationChainId, originToken, destinationToken);
}

Expand Down Expand Up @@ -241,11 +236,12 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
uint256 lpTokenAmount,
bool sendEth
) public nonReentrant {
// Can only send eth on withdrawing liquidity iff this is the WETH pool.
require(pooledTokens[l1Token].isWeth || !sendEth, "Cant send eth");
uint256 l1TokensToReturn = (lpTokenAmount * _exchangeRateCurrent(l1Token)) / 1e18;

ExpandedIERC20(pooledTokens[l1Token].lpToken).burnFrom(msg.sender, lpTokenAmount);
// Note this method does not make any liquidity utilization checks before letting the LP redeem their LP tokens.
// If they try access more funds that available (i.e l1TokensToReturn > liquidReserves) this will underflow.
Copy link
Member

Choose a reason for hiding this comment

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

this underflow is expected + fine in this case right?

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! solidity 8 protects against underflows so this simply reverts.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible for this to underflow, but the contract to have enough tokens to transfer out?

pooledTokens[l1Token].liquidReserves -= l1TokensToReturn;

if (sendEth) _unwrapWETHTo(l1Token, payable(msg.sender), l1TokensToReturn);
Expand All @@ -258,7 +254,21 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
return _exchangeRateCurrent(l1Token);
}

function liquidityUtilizationPostRelay(address token, uint256 relayedAmount) public returns (uint256) {}
function liquidityUtilizationCurrent(address l1Token) public nonReentrant returns (uint256) {
return _liquidityUtilizationPostRelay(l1Token, 0);
}

function liquidityUtilizationPostRelay(address l1Token, uint256 relayedAmount)
public
nonReentrant
returns (uint256)
{
return _liquidityUtilizationPostRelay(l1Token, relayedAmount);
}

function sync(address l1Token) public nonReentrant {
_sync(l1Token);
}

/*************************************************
* DATA WORKER FUNCTIONS *
Expand Down Expand Up @@ -490,7 +500,10 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
pooledTokens[l1Tokens[i]].liquidReserves -= uint256(netSendAmounts[i]);
}

// Assign any undistributed LP fees included into the bundle to the pooled token. Adding to the utilized reserves acts to track the fees while they are in transit and are not yet fully asigned during the smear.
// Assign any LP fees included into the bundle to the pooled token. These LP fees are tracked in the
// undistributedLpFees and within the utilizedReserves. undistributedLpFees is gradually decrease
// over the smear duration to give the LPs their rewards over a period of time. Adding to utilizedReserves
// acts to track these rewards after the smear duration. See _exchangeRateCurrent for more details.
pooledTokens[l1Tokens[i]].undistributedLpFees += bundleLpFees[i];
pooledTokens[l1Tokens[i]].utilizedReserves += int256(bundleLpFees[i]);
}
Expand All @@ -507,14 +520,17 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
function _exchangeRateCurrent(address l1Token) internal returns (uint256) {
PooledToken storage pooledToken = pooledTokens[l1Token]; // Note this is storage so the state can be modified.
uint256 lpTokenTotalSupply = IERC20(pooledToken.lpToken).totalSupply();
if (lpTokenTotalSupply == 0) return 1e18; // initial rate is 1 pre any mint action.
if (lpTokenTotalSupply == 0) return 1e18; // initial rate is 1:1 between LP tokens and collateral.

// First, update fee counters and local accounting of finalized transfers from L2 -> L1.
_updateAccumulatedLpFees(pooledToken); // Accumulate all allocated fees from the last time this method was called.
// _sync(); // Fetch any balance changes due to token bridging finalization and factor them in.
_sync(l1Token); // Fetch any balance changes due to token bridging finalization and factor them in.

// ExchangeRate := (liquidReserves + utilizedReserves - undistributedLpFees) / lpTokenSupply
// Note that utilizedReserves can be negative. If this is the case, then liquidReserves is offset by an equal
// Both utilizedReserves and undistributedLpFees contain assigned LP fees. UndistributedLpFees is gradually
// decreased over the smear duration using _updateAccumulatedLpFees. This means that the exchange rate will
// gradually increase over time as undistributedLpFees goes to zero.
// utilizedReserves can be negative. If this is the case, then liquidReserves is offset by an equal
// and opposite size. LiquidReserves + utilizedReserves will always be larger than undistributedLpFees so this
// int will always be positive so there is no risk in underflow in type casting in the return line.
int256 numerator = int256(pooledToken.liquidReserves) +
Expand All @@ -539,6 +555,38 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
return maxUndistributedLpFees < undistributedLpFees ? maxUndistributedLpFees : undistributedLpFees;
}

// Added to enable the BridgePool to receive ETH. used when unwrapping Weth.
function _sync(address l1Token) internal {
Copy link
Member Author

Choose a reason for hiding this comment

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

this is numerically identical to v1.

// Check if the l1Token balance of the contract is greater than the liquidReserves. If it is then the bridging
// action from L2 -> L1 has concluded and the local accounting can be updated.
uint256 l1TokenBalance = IERC20(l1Token).balanceOf(address(this));
if (l1TokenBalance > pooledTokens[l1Token].liquidReserves) {
// Note the numerical operation below can send utilizedReserves to negative. This can occur when tokens are
// dropped onto the contract, exceeding the liquidReserves.
pooledTokens[l1Token].utilizedReserves -= int256(l1TokenBalance - pooledTokens[l1Token].liquidReserves);
pooledTokens[l1Token].liquidReserves = l1TokenBalance;
}
}

function _liquidityUtilizationPostRelay(address l1Token, uint256 relayedAmount) internal returns (uint256) {
_sync(l1Token); // Fetch any balance changes due to token bridging finalization and factor them in.

// liquidityUtilizationRatio := (relayedAmount + max(utilizedReserves,0)) / (liquidReserves + max(utilizedReserves,0))
// UtilizedReserves has a dual meaning: if it's greater than zero then it represents funds pending in the bridge
// that will flow from L2 to L1. In this case, we can use it normally in the equation. However, if it is
// negative, then it is already counted in liquidReserves. This occurs if tokens are transferred directly to the
// contract. In this case, ignore it as it is captured in liquid reserves and has no meaning in the numerator.
PooledToken memory pooledToken = pooledTokens[l1Token]; // Note this is storage so the state can be modified.
uint256 flooredUtilizedReserves = pooledToken.utilizedReserves > 0 ? uint256(pooledToken.utilizedReserves) : 0;
uint256 numerator = relayedAmount + flooredUtilizedReserves;
uint256 denominator = pooledToken.liquidReserves + flooredUtilizedReserves;

// If the denominator equals zero, return 1e18 (max utilization).
if (denominator == 0) return 1e18;

// In all other cases, return the utilization ratio.
return (numerator * 1e18) / denominator;
}

// Added to enable the SpokePool to receive ETH. used when unwrapping WETH.
receive() external payable {}
}
93 changes: 93 additions & 0 deletions test/HubPool.LiquidityProvisionFees.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers } from "hardhat";
import { SignerWithAddress, seedWallet, toWei } from "./utils";
import * as consts from "./constants";
import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture";
import { constructSimple1ChainTree } from "./MerkleLib.utils";

let hubPool: Contract, weth: Contract, timer: Contract;
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;

describe("HubPool Liquidity Provision", function () {
beforeEach(async function () {
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
({ weth, hubPool, 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.equal(consts.amountToLp);
expect(pooledTokenInfoPreExecution.utilizedReserves).to.equal(0);
expect(pooledTokenInfoPreExecution.undistributedLpFees).to.equal(0);
expect(pooledTokenInfoPreExecution.lastLpFeeUpdate).to.equal(await timer.getCurrentTime());
expect(pooledTokenInfoPreExecution.isWeth).to.equal(true);

const { tokensSendToL2, realizedLpFees, leafs, tree } = await constructSimple1ChainTree(weth);

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.equal(consts.amountToLp.sub(tokensSendToL2));
// UtilizedReserves contains both the amount sent to L2 and the attributed LP fees.
expect(pooledTokenInfoPostExecution.utilizedReserves).to.equal(tokensSendToL2.add(realizedLpFees));
expect(pooledTokenInfoPostExecution.undistributedLpFees).to.equal(realizedLpFees);
});

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 constructSimple1ChainTree(weth);

// Exchange rate current before any fees are attributed execution should be 1.
expect(await hubPool.callStatic.exchangeRateCurrent(weth.address)).to.equal(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)).to.equal(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.equal(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)).to.equal(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.equal(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)).to.equal(toWei(1.01));
await hubPool.exchangeRateCurrent(weth.address); // force state sync.
expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.equal(0);
});
});
Loading