-
Notifications
You must be signed in to change notification settings - Fork 75
feat(hubPool): Add sync method and liquidity utilization #26
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 all 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
c46e94f
b884d2f
505ee77
986df76
2238088
5bd1473
408223c
24928d5
3d74944
08281ac
0a79da6
13b022b
edce0cc
3ded224
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,11 +51,11 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable { | |
| struct PooledToken { | ||
| address lpToken; | ||
| bool isEnabled; | ||
| uint256 liquidReserves; | ||
| bool isWeth; | ||
| uint32 lastLpFeeUpdate; | ||
| int256 utilizedReserves; | ||
| uint256 liquidReserves; | ||
| uint256 undistributedLpFees; | ||
| uint32 lastLpFeeUpdate; | ||
| bool isWeth; | ||
| } | ||
|
|
||
| mapping(address => PooledToken) public pooledTokens; | ||
|
|
@@ -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) | ||
|
Member
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. why remove comment? is this TODO resolved?
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. mistake in merging. added back. |
||
|
|
||
| whitelistedRoutes[originToken][destinationChainId] = destinationToken; | ||
| emit WhitelistRoute(destinationChainId, originToken, destinationToken); | ||
| } | ||
|
|
||
|
|
@@ -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. | ||
|
Member
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. this underflow is expected + fine in this case right?
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! solidity 8 protects against underflows so this simply reverts.
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. 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); | ||
|
|
@@ -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) { | ||
nicholaspai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 * | ||
|
|
@@ -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]); | ||
| } | ||
|
|
@@ -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) + | ||
|
|
@@ -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 { | ||
|
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. 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 {} | ||
| } | ||
| 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); | ||
| }); | ||
| }); |
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.
re-order for better packing.