-
Notifications
You must be signed in to change notification settings - Fork 75
feat (hubpool): Add protocol fee capture method #31
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
7240628
c3f90c3
8a9e37a
e2874df
9d4e545
ab18f89
c8bed30
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 |
|---|---|---|
|
|
@@ -64,7 +64,7 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
|
|
||
| mapping(uint256 => CrossChainContract) public crossChainContracts; // Mapping of chainId to the associated adapter and spokePool contracts. | ||
|
|
||
| LpTokenFactoryInterface lpTokenFactory; | ||
| LpTokenFactoryInterface public lpTokenFactory; | ||
|
|
||
| FinderInterface public finder; | ||
|
|
||
|
|
@@ -73,7 +73,15 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
|
|
||
| // Interest rate payment that scales the amount of pending fees per second paid to LPs. 0.0000015e18 will pay out | ||
| // the full amount of fees entitled to LPs in ~ 7.72 days, just over the standard L2 7 day liveness. | ||
| uint256 lpFeeRatePerSecond = 1500000000000; | ||
| uint256 public lpFeeRatePerSecond = 1500000000000; | ||
|
|
||
| mapping(address => uint256) public unclaimedAccumulatedProtocolFees; | ||
|
|
||
| // Address that captures protocol fees. Accumulated protocol fees can be claimed by this address. | ||
| address public protocolFeeCaptureAddress; | ||
|
|
||
| // Percentage of lpFees that are captured by the protocol and claimable by the protocolFeeCaptureAddress. | ||
| uint256 public protocolFeeCapturePct; | ||
|
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. Same, should change to uint64
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. Can we also put all the uint64 variables next to one another to decrease the cost of reading? Ideally, if we're smart about it, we can group variables that are often read (or written) in the same txn to reduce the overall number of words read/written. |
||
|
|
||
| // Token used to bond the data worker for proposing relayer refund bundles. | ||
| IERC20 public bondToken; | ||
|
|
@@ -85,6 +93,10 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| // be disputed only during this period of time. Defaults to 2 hours, like the rest of the UMA ecosystem. | ||
| uint64 public refundProposalLiveness = 7200; | ||
|
|
||
| event ProtocolFeeCaptureSet(address indexed newProtocolFeeCaptureAddress, uint256 indexed newProtocolFeeCapturePct); | ||
|
|
||
| event ProtocolFeesCapturedClaimed(address indexed l1Token, uint256 indexed accumulatedFees); | ||
|
|
||
| event BondSet(address indexed newBondToken, uint256 newBondAmount); | ||
|
|
||
| event RefundProposalLivenessSet(uint256 newRefundProposalLiveness); | ||
|
|
@@ -144,12 +156,23 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| ) Testable(_timer) { | ||
| lpTokenFactory = _lpTokenFactory; | ||
| finder = _finder; | ||
| protocolFeeCaptureAddress = owner(); | ||
| } | ||
|
|
||
| /************************************************* | ||
| * ADMIN FUNCTIONS * | ||
| *************************************************/ | ||
|
|
||
| function setProtocolFeeCapture(address newProtocolFeeCaptureAddress, uint256 newProtocolFeeCapturePct) | ||
| public | ||
| onlyOwner | ||
| { | ||
| require(newProtocolFeeCapturePct <= 1e18, "Bad protocolFeeCapturePct"); | ||
| protocolFeeCaptureAddress = newProtocolFeeCaptureAddress; | ||
| protocolFeeCapturePct = newProtocolFeeCapturePct; | ||
|
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. Should we check that this is <= 1e18? |
||
| emit ProtocolFeeCaptureSet(newProtocolFeeCaptureAddress, newProtocolFeeCapturePct); | ||
| } | ||
|
|
||
| function setBond(IERC20 newBondToken, uint256 newBondAmount) public onlyOwner noActiveRequests { | ||
| bondToken = newBondToken; | ||
| bondAmount = newBondAmount; | ||
|
|
@@ -281,7 +304,7 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| bytes32 poolRebalanceRoot, | ||
| bytes32 destinationDistributionRoot, | ||
| bytes32 slowRelayFulfillmentRoot | ||
| ) public noActiveRequests { | ||
| ) public nonReentrant noActiveRequests { | ||
|
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. modifiers were missing. |
||
| require(poolRebalanceLeafCount > 0, "Bundle must have at least 1 leaf"); | ||
|
|
||
| uint64 requestExpirationTimestamp = uint64(getCurrentTime() + refundProposalLiveness); | ||
|
|
@@ -309,7 +332,10 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| ); | ||
| } | ||
|
|
||
| function executeRelayerRefund(PoolRebalanceLeaf memory poolRebalanceLeaf, bytes32[] memory proof) public { | ||
| function executeRelayerRefund(PoolRebalanceLeaf memory poolRebalanceLeaf, bytes32[] memory proof) | ||
| public | ||
| nonReentrant | ||
| { | ||
| require(getCurrentTime() >= refundRequest.requestExpirationTimestamp, "Not passed liveness"); | ||
|
|
||
| // Verify the leafId in the poolRebalanceLeaf has not yet been claimed. | ||
|
|
@@ -349,7 +375,7 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| ); | ||
| } | ||
|
|
||
| function disputeRelayerRefund() public { | ||
| function disputeRelayerRefund() public nonReentrant { | ||
| require(getCurrentTime() <= refundRequest.requestExpirationTimestamp, "Request passed liveness"); | ||
|
|
||
| // Request price from OO and dispute it. | ||
|
|
@@ -405,6 +431,12 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| delete refundRequest; | ||
| } | ||
|
|
||
| function claimProtocolFeesCaptured(address l1Token) public nonReentrant { | ||
| IERC20(l1Token).safeTransfer(protocolFeeCaptureAddress, unclaimedAccumulatedProtocolFees[l1Token]); | ||
| emit ProtocolFeesCapturedClaimed(l1Token, unclaimedAccumulatedProtocolFees[l1Token]); | ||
| unclaimedAccumulatedProtocolFees[l1Token] = 0; | ||
| } | ||
|
|
||
| function _getRefundProposalAncillaryData() public view returns (bytes memory ancillaryData) { | ||
| ancillaryData = AncillaryData.appendKeyValueUint( | ||
| "", | ||
|
|
@@ -501,12 +533,8 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| pooledTokens[l1Tokens[i]].liquidReserves -= uint256(netSendAmounts[i]); | ||
| } | ||
|
|
||
| // 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]); | ||
| // Allocate LP fees and protocol fees from the bundle to the associated pooled token trackers. | ||
| _allocateLpAndProtocolFees(l1Tokens[i], bundleLpFees[i]); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -592,6 +620,24 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable { | |
| return (numerator * 1e18) / denominator; | ||
| } | ||
|
|
||
| function _allocateLpAndProtocolFees(address l1Token, uint256 bundleLpFees) internal { | ||
| // Calculate the fraction of bundledLpFees that are allocated to the protocol and to the LPs. | ||
| uint256 protocolFeesCaptured = (bundleLpFees * protocolFeeCapturePct) / 1e18; | ||
| uint256 lpFeesCaptured = bundleLpFees - protocolFeesCaptured; | ||
|
|
||
| // 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. | ||
| if (lpFeesCaptured > 0) { | ||
| pooledTokens[l1Token].undistributedLpFees += lpFeesCaptured; | ||
| pooledTokens[l1Token].utilizedReserves += int256(lpFeesCaptured); | ||
| } | ||
|
|
||
| // If there are any protocol fees, allocate them to the unclaimed protocol tracker amount. | ||
| if (protocolFeesCaptured > 0) unclaimedAccumulatedProtocolFees[l1Token] += protocolFeesCaptured; | ||
| } | ||
|
|
||
| // Added to enable the SpokePool to receive ETH. used when unwrapping WETH. | ||
| receive() external payable {} | ||
| } | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { toWei, toBNWei, SignerWithAddress, seedWallet, expect, Contract, ethers } from "./utils"; | ||
| import { mockTreeRoot, finalFee, bondAmount, amountToLp, refundProposalLiveness } from "./constants"; | ||
| import { hubPoolFixture, enableTokensForLP } from "./HubPool.Fixture"; | ||
| import { constructSingleChainTree } from "./MerkleLib.utils"; | ||
|
|
||
| let hubPool: Contract, weth: Contract, timer: Contract; | ||
| let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; | ||
|
|
||
| const initialProtocolFeeCapturePct = toBNWei("0.1"); | ||
|
|
||
| describe("HubPool Protocol fees", function () { | ||
| beforeEach(async function () { | ||
| [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); | ||
| ({ weth, hubPool, timer } = await hubPoolFixture()); | ||
| await seedWallet(dataWorker, [], weth, bondAmount.add(finalFee).mul(2)); | ||
| await seedWallet(liquidityProvider, [], weth, amountToLp.mul(10)); | ||
|
|
||
| await enableTokensForLP(owner, hubPool, weth, [weth]); | ||
| await weth.connect(liquidityProvider).approve(hubPool.address, amountToLp); | ||
| await hubPool.connect(liquidityProvider).addLiquidity(weth.address, amountToLp); | ||
| await weth.connect(dataWorker).approve(hubPool.address, bondAmount.mul(10)); | ||
|
|
||
| await hubPool.setProtocolFeeCapture(owner.address, initialProtocolFeeCapturePct); | ||
| }); | ||
|
|
||
| it("Only owner can set protocol fee capture", async function () { | ||
| await expect(hubPool.connect(liquidityProvider).setProtocolFeeCapture(liquidityProvider.address, toWei("0.1"))).to | ||
| .be.reverted; | ||
| }); | ||
| it("Can change protocol fee capture settings", async function () { | ||
| expect(await hubPool.callStatic.protocolFeeCaptureAddress()).to.equal(owner.address); | ||
| expect(await hubPool.callStatic.protocolFeeCapturePct()).to.equal(initialProtocolFeeCapturePct); | ||
| const newPct = toWei("0.1"); | ||
| await hubPool.connect(owner).setProtocolFeeCapture(liquidityProvider.address, newPct); | ||
| expect(await hubPool.callStatic.protocolFeeCaptureAddress()).to.equal(liquidityProvider.address); | ||
| expect(await hubPool.callStatic.protocolFeeCapturePct()).to.equal(newPct); | ||
| }); | ||
| it("When fee capture pct is not set to zero fees correctly attribute between LPs and the protocol", async function () { | ||
| const { leafs, tree, realizedLpFees } = await constructSingleChainTree(weth); | ||
| await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), mockTreeRoot, mockTreeRoot); | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness); | ||
| await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); | ||
|
|
||
| // 90% of the fees should be attributed to the LPs. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.equal( | ||
| realizedLpFees.mul(toBNWei("1").sub(initialProtocolFeeCapturePct)).div(toBNWei("1")) | ||
| ); | ||
|
|
||
| // 10% of the fees should be attributed to the protocol. | ||
| const expectedProtocolFees = realizedLpFees.mul(initialProtocolFeeCapturePct).div(toBNWei("1")); | ||
| expect(await hubPool.unclaimedAccumulatedProtocolFees(weth.address)).to.equal(expectedProtocolFees); | ||
|
|
||
| // Protocol should be able to claim their fees. | ||
| await expect(() => hubPool.claimProtocolFeesCaptured(weth.address)).to.changeTokenBalance( | ||
| weth, | ||
| owner, | ||
| expectedProtocolFees | ||
| ); | ||
|
|
||
| // After claiming, the protocol fees should be zero. | ||
| expect(await hubPool.unclaimedAccumulatedProtocolFees(weth.address)).to.equal("0"); | ||
|
|
||
| // Once all the fees have been attributed the correct amount should be claimable by the LPs. | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 10 * 24 * 60 * 60); // Move time to accumulate all fees. | ||
| await hubPool.exchangeRateCurrent(weth.address); // force state sync. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.equal(0); | ||
| }); | ||
| it("When fee capture pct is set to zero all fees accumulate to the LPs", async function () { | ||
| await hubPool.setProtocolFeeCapture(owner.address, "0"); | ||
| const { leafs, tree, realizedLpFees } = await constructSingleChainTree(weth); | ||
| await hubPool.connect(dataWorker).initiateRelayerRefund([3117], 1, tree.getHexRoot(), mockTreeRoot, mockTreeRoot); | ||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + refundProposalLiveness); | ||
| await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0])); | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.equal(realizedLpFees); | ||
|
|
||
| await timer.setCurrentTime(Number(await timer.getCurrentTime()) + 10 * 24 * 60 * 60); // Move time to accumulate all fees. | ||
| await hubPool.exchangeRateCurrent(weth.address); // force state sync. | ||
| expect((await hubPool.pooledTokens(weth.address)).undistributedLpFees).to.equal(0); | ||
| expect(await hubPool.callStatic.exchangeRateCurrent(weth.address)).to.equal(toWei(1.01)); | ||
| }); | ||
| }); |
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.
this should be public.
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.
can we make this uint64 to match the other pcts?