Skip to content

Commit 3c22f14

Browse files
chrismareemrice32
andauthored
feat(HubPool): Second iteration on init, dispute and execute relayer refund bundles (#13)
* nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * Update contracts/HubPool.sol Co-authored-by: Matt Rice <matthewcrice32@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> Co-authored-by: Matt Rice <matthewcrice32@gmail.com>
1 parent 448680b commit 3c22f14

File tree

6 files changed

+142
-54
lines changed

6 files changed

+142
-54
lines changed

contracts/HubPool.sol

Lines changed: 99 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: GPL-3.0-only
22
pragma solidity ^0.8.0;
33

4+
import "./MerkleLib.sol";
5+
46
import "@uma/core/contracts/common/implementation/Testable.sol";
57
import "@uma/core/contracts/common/implementation/Lockable.sol";
68
import "@uma/core/contracts/common/implementation/MultiCaller.sol";
@@ -29,16 +31,14 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
2931
bool isEnabled;
3032
}
3133

32-
enum RefundRequestStatus {
33-
Pending,
34-
Finalized,
35-
Disputed
36-
}
37-
3834
struct RelayerRefundRequest {
39-
bytes32 poolRebalanceProof;
40-
bytes32 destinationDistributionProof;
41-
RefundRequestStatus status;
35+
uint64 requestExpirationTimestamp;
36+
uint64 unclaimedPoolRebalanceLeafs;
37+
bytes32 poolRebalanceRoot;
38+
bytes32 destinationDistributionRoot;
39+
mapping(uint256 => uint256) claimedBitMap;
40+
address proposer;
41+
bool proposerBondRepaid;
4242
}
4343

4444
RelayerRefundRequest[] public relayerRefundRequests;
@@ -57,8 +57,9 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
5757
// The computed bond amount as the UMA Store's final fee multiplied by the bondTokenFinalFeeMultiplier.
5858
uint256 public bondAmount;
5959

60-
// There should only ever be one refund proposal pending at any point in time. This bool toggles accordingly.
61-
bool public currentPendingRefundProposal = false;
60+
// Each refund proposal must stay in liveness for this period of time before it can be considered finalized. It can
61+
// be disputed only during this period of time.
62+
uint64 public refundProposalLiveness;
6263

6364
event BondAmountSet(uint64 newBondMultiplier);
6465

@@ -78,21 +79,28 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
7879
);
7980
event WhitelistRoute(address originToken, uint256 destinationChainId, address destinationToken);
8081

81-
event RelayerRefundRequested(
82-
uint64 relayerRefundId,
82+
event InitiateRefundRequested(
83+
uint64 indexed relayerRefundId,
84+
uint64 requestExpirationTimestamp,
85+
uint64 poolRebalanceLeafCount,
8386
uint256[] bundleEvaluationBlockNumbers,
84-
bytes32 indexed poolRebalanceProof,
85-
bytes32 indexed destinationDistributionProof,
86-
address proposer
87+
bytes32 poolRebalanceRoot,
88+
bytes32 destinationDistributionRoot,
89+
address indexed proposer
8790
);
91+
event RelayerRefundExecuted(uint256 relayerRefundId, MerkleLib.PoolRebalance poolRebalance, address caller);
92+
93+
event RelayerRefundDisputed(uint256 relayerRefundId, address disputer);
8894

8995
constructor(
9096
uint256 _bondAmount,
97+
uint64 _refundProposalLiveness,
9198
address _bondToken,
9299
address _l1Weth,
93100
address _timerAddress
94101
) Testable(_timerAddress) {
95102
bondAmount = _bondAmount;
103+
refundProposalLiveness = _refundProposalLiveness;
96104
bondToken = IERC20(_bondToken);
97105
l1Weth = WETH9Like(_l1Weth);
98106
}
@@ -201,48 +209,101 @@ contract HubPool is Testable, Lockable, MultiCaller, Ownable {
201209

202210
function initiateRelayerRefund(
203211
uint256[] memory bundleEvaluationBlockNumbers,
204-
bytes32 poolRebalanceProof,
205-
bytes32 destinationDistributionProof
212+
uint64 poolRebalanceLeafCount,
213+
bytes32 poolRebalanceRoot,
214+
bytes32 destinationDistributionRoot
206215
) public {
207-
require(currentPendingRefundProposal == false, "Only one proposal at a time");
208-
currentPendingRefundProposal = true;
209-
relayerRefundRequests.push(
210-
RelayerRefundRequest({
211-
poolRebalanceProof: poolRebalanceProof,
212-
destinationDistributionProof: destinationDistributionProof,
213-
status: RefundRequestStatus.Pending
214-
})
216+
// The most recent refund proposal must be fully claimed before the next relayer refund bundle is initiated.
217+
require(
218+
relayerRefundRequests.length == 0 || // If this is the first initiated relay refund.
219+
relayerRefundRequests[getNumberOfRelayRefunds()].unclaimedPoolRebalanceLeafs == 0,
220+
"Last bundle has unclaimed leafs"
215221
);
216222

223+
uint64 requestExpirationTimestamp = uint64(getCurrentTime() + refundProposalLiveness);
224+
225+
RelayerRefundRequest storage relayerRefundRequest = relayerRefundRequests.push();
226+
relayerRefundRequest.requestExpirationTimestamp = requestExpirationTimestamp;
227+
relayerRefundRequest.unclaimedPoolRebalanceLeafs = poolRebalanceLeafCount;
228+
relayerRefundRequest.poolRebalanceRoot = poolRebalanceRoot;
229+
relayerRefundRequest.destinationDistributionRoot = destinationDistributionRoot;
230+
relayerRefundRequest.proposer = msg.sender;
231+
217232
// Pull bondAmount of bondToken from the caller.
218233
bondToken.safeTransferFrom(msg.sender, address(this), bondAmount);
219234

220-
emit RelayerRefundRequested(
221-
uint64(relayerRefundRequests.length - 1), // Index of the relayerRefundRequest within the array.
235+
emit InitiateRefundRequested(
236+
uint64(relayerRefundRequests.length - 1),
237+
requestExpirationTimestamp,
238+
poolRebalanceLeafCount,
222239
bundleEvaluationBlockNumbers,
223-
poolRebalanceProof,
224-
destinationDistributionProof,
240+
poolRebalanceRoot,
241+
destinationDistributionRoot,
225242
msg.sender
226243
);
227244
}
228245

229246
function executeRelayerRefund(
230247
uint256 relayerRefundRequestId,
231-
uint256 leafId,
232-
uint256 repaymentChainId,
233-
address[] memory l1TokenAddress,
234-
uint256[] memory accumulatedLpFees,
235-
uint256[] memory netSendAmounts,
236-
bytes32[] memory inclusionProof
237-
) public {}
248+
MerkleLib.PoolRebalance memory poolRebalance,
249+
bytes32[] memory proof
250+
) public {
251+
RelayerRefundRequest storage relayerRefund = relayerRefundRequests[relayerRefundRequestId];
252+
require(getCurrentTime() > relayerRefund.requestExpirationTimestamp, "Not passed liveness");
253+
254+
// Verify the leafId in the poolRebalance has not yet been claimed.
255+
require(!MerkleLib.isClaimed(relayerRefund.claimedBitMap, poolRebalance.leafId), "Already claimed");
256+
257+
// Verify the props provided generate a leaf that, along with the proof, are included in the merkle root.
258+
require(MerkleLib.verifyPoolRebalance(relayerRefund.poolRebalanceRoot, poolRebalance, proof), "Bad Proof");
238259

239-
function disputeRelayerRefund() public {}
260+
// Set the leafId in the claimed bitmap.
261+
MerkleLib.setClaimed(relayerRefund.claimedBitMap, poolRebalance.leafId);
262+
263+
// Decrement the unclaimedPoolRebalanceLeafs.
264+
relayerRefund.unclaimedPoolRebalanceLeafs--;
265+
266+
// Transfer the bondAmount to back to the proposer, if this was not done before for this refund bundle.
267+
if (!relayerRefund.proposerBondRepaid) {
268+
relayerRefund.proposerBondRepaid = true;
269+
bondToken.safeTransfer(relayerRefund.proposer, bondAmount);
270+
}
271+
272+
// TODO call into canonical bridge to send PoolRebalance.netSendAmount for the associated
273+
// PoolRebalance.tokenAddresses, to the target PoolRebalance.chainId. this will likely happen within a
274+
// x_Messenger contract for each chain. these messengers will be registered in a separate process that will follow
275+
// in a later PR.
276+
// TODO: modify the associated utilized and pending reserves for each token sent.
277+
278+
emit RelayerRefundExecuted(relayerRefundRequestId, poolRebalance, msg.sender);
279+
}
280+
281+
function disputeRelayerRefund(uint256 relayerRefundRequestId) public {
282+
RelayerRefundRequest storage relayerRefund = relayerRefundRequests[relayerRefundRequestId];
283+
require(
284+
getCurrentTime() > relayerRefundRequests[relayerRefundRequestId].requestExpirationTimestamp,
285+
"Passed liveness"
286+
);
287+
288+
// Delete the last element in the relayerRefundRequests array. This acts to throw out the request.
289+
emit RelayerRefundDisputed(relayerRefundRequestId, msg.sender);
290+
291+
relayerRefundRequests.pop();
292+
293+
// TODO: pull bonds. request price from OO.
294+
}
295+
296+
function getNumberOfRelayRefunds() public view returns (uint256) {
297+
if (relayerRefundRequests.length == 0) return 0;
298+
return relayerRefundRequests.length - 1;
299+
}
240300

241301
/*************************************************
242302
* INTERNAL FUNCTIONS *
243303
*************************************************/
244304

245305
function _exchangeRateCurrent() internal pure returns (uint256) {
306+
// TODO: implement this method to consider utilization.
246307
return 1e18;
247308
}
248309

contracts/MerkleLib.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ library MerkleLib {
5757
* @param repayment the repayment struct.
5858
* @param proof the merkle proof.
5959
*/
60-
function verifyRepayment(
60+
function verifyPoolRebalance(
6161
bytes32 root,
6262
PoolRebalance memory repayment,
6363
bytes32[] memory proof
@@ -71,7 +71,7 @@ library MerkleLib {
7171
* @param distribution the distribution struct.
7272
* @param proof the merkle proof.
7373
*/
74-
function verifyDistribution(
74+
function verifyRelayerDistribution(
7575
bytes32 root,
7676
DestinationDistribution memory distribution,
7777
bytes32[] memory proof

test/HubPool.Fixture.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TokenRolesEnum } from "@uma/common";
22
import { getContractFactory } from "./utils";
3-
import { bondAmount } from "./constants";
3+
import { bondAmount, refundProposalLiveness } from "./constants";
44
import { Contract } from "ethers";
55

66
export async function deployHubPoolTestHelperContracts(deployerWallet: any) {
@@ -17,7 +17,7 @@ export async function deployHubPoolTestHelperContracts(deployerWallet: any) {
1717
// Deploy the hubPool
1818
const hubPool = await (
1919
await getContractFactory("HubPool", deployerWallet)
20-
).deploy(bondAmount, weth.address, weth.address, timer.address);
20+
).deploy(bondAmount, refundProposalLiveness, weth.address, weth.address, timer.address);
2121

2222
return { timer, weth, usdc, dai, hubPool };
2323
}

test/HubPool.RelayerRefund.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Contract } from "ethers";
44
import { ethers } from "hardhat";
55
import { ZERO_ADDRESS } from "@uma/common";
66
import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils";
7-
import { depositDestinationChainId, bondAmount } from "./constants";
7+
import { depositDestinationChainId, bondAmount, refundProposalLiveness } from "./constants";
88
import { deployHubPoolTestHelperContracts } from "./HubPool.Fixture";
99

1010
let hubPool: Contract, weth: Contract, usdc: Contract;
@@ -19,26 +19,46 @@ describe("HubPool Relayer Refund", function () {
1919

2020
it("Initialization of a relay correctly stores data, emits events and pulls the bond", async function () {
2121
const bundleEvaluationBlockNumbers = [1, 2, 3];
22+
const poolRebalanceLeafCount = 5;
2223
const poolRebalanceProof = createRandomBytes32();
2324
const destinationDistributionProof = createRandomBytes32();
2425

26+
const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + refundProposalLiveness;
2527
await weth.connect(dataWorker).approve(hubPool.address, bondAmount);
2628
await expect(
2729
hubPool
2830
.connect(dataWorker)
29-
.initiateRelayerRefund(bundleEvaluationBlockNumbers, poolRebalanceProof, destinationDistributionProof)
31+
.initiateRelayerRefund(
32+
bundleEvaluationBlockNumbers,
33+
poolRebalanceLeafCount,
34+
poolRebalanceProof,
35+
destinationDistributionProof
36+
)
3037
)
31-
.to.emit(hubPool, "RelayerRefundRequested")
32-
.withArgs(0, bundleEvaluationBlockNumbers, poolRebalanceProof, destinationDistributionProof, dataWorker.address);
38+
.to.emit(hubPool, "InitiateRefundRequested")
39+
.withArgs(
40+
0,
41+
expectedRequestExpirationTimestamp,
42+
poolRebalanceLeafCount,
43+
bundleEvaluationBlockNumbers,
44+
poolRebalanceProof,
45+
destinationDistributionProof,
46+
dataWorker.address
47+
);
3348
// Balances of the hubPool should have incremented by the bond and the dataWorker should have decremented by the bond.
3449
expect(await weth.balanceOf(hubPool.address)).to.equal(bondAmount);
3550
expect(await weth.balanceOf(dataWorker.address)).to.equal(0);
3651

37-
// Can only have one pending proposal at a time.
52+
// Can not re-initialize if the previous bundle has unclaimed leaves.
3853
await expect(
3954
hubPool
4055
.connect(dataWorker)
41-
.initiateRelayerRefund(bundleEvaluationBlockNumbers, poolRebalanceProof, destinationDistributionProof)
42-
).to.be.revertedWith("Only one proposal at a time");
56+
.initiateRelayerRefund(
57+
bundleEvaluationBlockNumbers,
58+
poolRebalanceLeafCount,
59+
poolRebalanceProof,
60+
destinationDistributionProof
61+
)
62+
).to.be.revertedWith("Last bundle has unclaimed leafs");
4363
});
4464
});

test/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export const depositRelayerFeePct = toWei("0.25");
1313
export const depositQuoteTimeBuffer = 10 * 60; // 10 minutes
1414

1515
export const bondAmount = toWei("5"); // 5 ETH as the bond for proposing refund bundles.
16+
17+
export const refundProposalLiveness = 100;

test/utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { getBytecode, getAbi } from "@uma/contracts-node";
22
import { ethers } from "hardhat";
3-
import { BigNumber, Signer, Contract } from "ethers";
3+
import { BigNumber, Signer, Contract, ContractFactory } from "ethers";
44

55
export interface SignerWithAddress extends Signer {
66
address: string;
77
}
88

9-
export async function getContractFactory(name: string, signer: SignerWithAddress) {
9+
export async function getContractFactory(name: string, signer: SignerWithAddress): Promise<ContractFactory> {
1010
try {
1111
// Try fetch from the local ethers factory from HRE. If this exists then the contract is in this package.
12+
if (name == "HubPool") {
13+
const merkleLib = await (await ethers.getContractFactory("MerkleLib")).deploy();
14+
return await ethers.getContractFactory(name, { libraries: { MerkleLib: merkleLib.address } });
15+
}
1216
return await ethers.getContractFactory(name);
13-
} catch (error) {}
14-
// If it does not exist then try find the contract in the UMA core package.
15-
return new ethers.ContractFactory(getAbi(name as any), getBytecode(name as any), signer);
17+
} catch (error) {
18+
// If it does not exist then try find the contract in the UMA core package.
19+
return new ethers.ContractFactory(getAbi(name as any), getBytecode(name as any), signer);
20+
}
1621
}
1722

1823
export const toWei = (num: string | number | BigNumber) => ethers.utils.parseEther(num.toString());

0 commit comments

Comments
 (0)