Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
68 changes: 57 additions & 11 deletions contracts/HubPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
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 should be public.

Copy link
Member

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?


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;
Copy link
Member

Choose a reason for hiding this comment

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

Same, should change to uint64

Copy link
Contributor

@mrice32 mrice32 Feb 14, 2022

Choose a reason for hiding this comment

The 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;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -281,7 +304,7 @@ contract HubPool is HubPoolInterface, Testable, Lockable, MultiCaller, Ownable {
bytes32 poolRebalanceRoot,
bytes32 destinationDistributionRoot,
bytes32 slowRelayFulfillmentRoot
) public noActiveRequests {
) public nonReentrant noActiveRequests {
Copy link
Member Author

Choose a reason for hiding this comment

The 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);
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
"",
Expand Down Expand Up @@ -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]);
}
}

Expand Down Expand Up @@ -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 {}
}
108 changes: 0 additions & 108 deletions test/HubPool.Fees.ts

This file was deleted.

81 changes: 81 additions & 0 deletions test/HubPool.ProtocolFees.ts
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));
});
});
1 change: 0 additions & 1 deletion test/chain-adapters/Arbitrum_Adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ describe("Arbitrum Chain Adapter", function () {
await hubPool.connect(dataWorker).executeRelayerRefund(leafs[0], tree.getHexProof(leafs[0]));
// The correct functions should have been called on the arbitrum contracts.
expect(l1ERC20Gateway.outboundTransfer).to.have.been.calledOnce; // One token transfer over the canonical bridge.

expect(l1ERC20Gateway.outboundTransfer).to.have.been.calledWith(
dai.address,
mockSpoke.address,
Expand Down