Skip to content

Commit e46a6cc

Browse files
chrismareemrice32
andauthored
feat: hubpool third iteration. Add dispute logic and refine bitmap structure. (#14)
* 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> * feat(hubpool): Refactor hub pool to use 1D bitmap integer Signed-off-by: chrismaree <christopher.maree@gmail.com> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP Signed-off-by: chrismaree <christopher.maree@gmail.com> * review nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * review 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> * 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> * nit Signed-off-by: chrismaree <christopher.maree@gmail.com> * WIP 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> Co-authored-by: Matt Rice <matthewcrice32@gmail.com>
1 parent 7ab33c8 commit e46a6cc

13 files changed

+439
-147
lines changed

contracts/BridgeAdmin.sol

Lines changed: 0 additions & 13 deletions
This file was deleted.

contracts/HubPool.sol

Lines changed: 146 additions & 66 deletions
Large diffs are not rendered by default.

contracts/MerkleLib.sol

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ library MerkleLib {
8686
* @notice Tests whether a claim is contained within a claimedBitMap mapping.
8787
* @param claimedBitMap a simple uint256 mapping in storage used as a bitmap.
8888
* @param index the index to check in the bitmap.
89+
* @return bool indicating if the index within the claimedBitMap has been marked as claimed.
8990
*/
9091
function isClaimed(mapping(uint256 => uint256) storage claimedBitMap, uint256 index) public view returns (bool) {
9192
uint256 claimedWordIndex = index / 256;
@@ -105,4 +106,25 @@ library MerkleLib {
105106
uint256 claimedBitIndex = index % 256;
106107
claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
107108
}
109+
110+
/**
111+
* @notice Tests whether a claim is contained within a 1D claimedBitMap mapping.
112+
* @param claimedBitMap a simple uint256 value, encoding a 1D bitmap.
113+
* @param index the index to check in the bitmap.
114+
\* @return bool indicating if the index within the claimedBitMap has been marked as claimed.
115+
*/
116+
function isClaimed1D(uint256 claimedBitMap, uint256 index) public pure returns (bool) {
117+
uint256 mask = (1 << index);
118+
return claimedBitMap & mask == mask;
119+
}
120+
121+
/**
122+
* @notice Marks an index in a claimedBitMap as claimed.
123+
* @param claimedBitMap a simple uint256 mapping in storage used as a bitmap.
124+
* @param index the index to mark in the bitmap.
125+
*/
126+
function setClaimed1D(uint256 claimedBitMap, uint256 index) public pure returns (uint256) {
127+
require(index <= 255, "Index out of bounds");
128+
return claimedBitMap | (1 << index % 256);
129+
}
108130
}

contracts/interfaces/BridgeAdminInterface.sol

Lines changed: 0 additions & 6 deletions
This file was deleted.

contracts/test/MerkleLibTest.sol

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import "../MerkleLib.sol";
99
contract MerkleLibTest {
1010
mapping(uint256 => uint256) public claimedBitMap;
1111

12+
uint256 public claimedBitMap1D;
13+
1214
function verifyPoolRebalance(
1315
bytes32 root,
1416
MerkleLib.PoolRebalance memory rebalance,
@@ -32,4 +34,12 @@ contract MerkleLibTest {
3234
function setClaimed(uint256 index) public {
3335
MerkleLib.setClaimed(claimedBitMap, index);
3436
}
37+
38+
function isClaimed1D(uint256 index) public view returns (bool) {
39+
return MerkleLib.isClaimed1D(claimedBitMap1D, index);
40+
}
41+
42+
function setClaimed1D(uint256 index) public {
43+
claimedBitMap1D = MerkleLib.setClaimed1D(claimedBitMap1D, index);
44+
}
3545
}

test/HubPool.Admin.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import { expect } from "chai";
22
import { Contract } from "ethers";
33
import { ethers } from "hardhat";
44
import { ZERO_ADDRESS } from "@uma/common";
5-
import { getContractFactory, SignerWithAddress } from "./utils";
6-
import { depositDestinationChainId } from "./constants";
5+
import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils";
6+
import { depositDestinationChainId, bondAmount } from "./constants";
77
import { hubPoolFixture } from "./HubPool.Fixture";
88

99
let hubPool: Contract, weth: Contract, usdc: Contract;
1010
let owner: SignerWithAddress, other: SignerWithAddress;
1111

1212
describe("HubPool Admin functions", function () {
13-
before(async function () {
13+
beforeEach(async function () {
1414
[owner, other] = await ethers.getSigners();
1515
({ weth, hubPool, usdc } = await hubPoolFixture());
1616
});
@@ -43,4 +43,21 @@ describe("HubPool Admin functions", function () {
4343
.withArgs(weth.address, depositDestinationChainId, usdc.address);
4444
expect(await hubPool.whitelistedRoutes(weth.address, depositDestinationChainId)).to.equal(usdc.address);
4545
});
46+
47+
it("Can change the bond token and amount", async function () {
48+
expect(await hubPool.callStatic.bondToken()).to.equal(weth.address); // Default set in the fixture.
49+
expect(await hubPool.callStatic.bondAmount()).to.equal(bondAmount); // Default set in the fixture.
50+
51+
// Set the bond token and amount to 1000 USDC
52+
const newBondAmount = ethers.utils.parseUnits("1000", 6); // set to 1000e6, i.e 1000 USDC.
53+
await hubPool.setBond(usdc.address, newBondAmount);
54+
expect(await hubPool.callStatic.bondToken()).to.equal(usdc.address); // New Address.
55+
expect(await hubPool.callStatic.bondAmount()).to.equal(newBondAmount); // New Bond amount.
56+
});
57+
it("Can not change the bond token and amount during a pending refund", async function () {
58+
await seedWallet(owner, [], weth, bondAmount);
59+
await weth.approve(hubPool.address, bondAmount);
60+
await hubPool.initiateRelayerRefund([1, 2, 3], 5, createRandomBytes32(), createRandomBytes32());
61+
await expect(hubPool.setBond(usdc.address, "1")).to.be.revertedWith("Active request has unclaimed leafs");
62+
});
4663
});

test/HubPool.Fixture.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,50 @@
1-
import { TokenRolesEnum } from "@uma/common";
2-
import { getContractFactory } from "./utils";
3-
import { bondAmount, refundProposalLiveness } from "./constants";
1+
import { TokenRolesEnum, interfaceName } from "@uma/common";
2+
import { getContractFactory, utf8ToHex, toBN, fromWei } from "./utils";
3+
import { bondAmount, refundProposalLiveness, finalFee, identifier } from "./constants";
44
import { Contract, Signer } from "ethers";
55
import hre from "hardhat";
66

7+
import { umaEcosystemFixture } from "./UmaEcosystem.Fixture";
8+
79
export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) => {
8-
const [deployerWallet] = await ethers.getSigners();
10+
const [signer] = await ethers.getSigners();
911

10-
// Useful contracts.
11-
const timer = await (await getContractFactory("Timer", deployerWallet)).deploy();
12+
// This fixture is dependent on the UMA ecosystem fixture. Run it first and grab the output. This is used in the
13+
// deployments that follows. The output is spread when returning contract instances from this fixture.
14+
const parentFixtureOutput = await umaEcosystemFixture();
1215

1316
// Create 3 tokens: WETH for wrapping unwrapping and 2 ERC20s with different decimals.
14-
const weth = await (await getContractFactory("WETH9", deployerWallet)).deploy();
15-
const usdc = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("USD Coin", "USDC", 6);
16-
await usdc.addMember(TokenRolesEnum.MINTER, deployerWallet.address);
17-
const dai = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("DAI Stablecoin", "DAI", 18);
18-
await dai.addMember(TokenRolesEnum.MINTER, deployerWallet.address);
17+
const weth = await (await getContractFactory("WETH9", signer)).deploy();
18+
const usdc = await (await getContractFactory("ExpandedERC20", signer)).deploy("USD Coin", "USDC", 6);
19+
await usdc.addMember(TokenRolesEnum.MINTER, signer.address);
20+
const dai = await (await getContractFactory("ExpandedERC20", signer)).deploy("DAI Stablecoin", "DAI", 18);
21+
await dai.addMember(TokenRolesEnum.MINTER, signer.address);
22+
23+
// Set the above currencies as approved in the UMA collateralWhitelist.
24+
await parentFixtureOutput.collateralWhitelist.addToWhitelist(weth.address);
25+
await parentFixtureOutput.collateralWhitelist.addToWhitelist(usdc.address);
26+
await parentFixtureOutput.collateralWhitelist.addToWhitelist(dai.address);
27+
28+
// Set the finalFee for all the new tokens.
29+
await parentFixtureOutput.store.setFinalFee(weth.address, { rawValue: finalFee });
30+
await parentFixtureOutput.store.setFinalFee(usdc.address, { rawValue: toBN(fromWei(finalFee)).mul(1e6) });
31+
await parentFixtureOutput.store.setFinalFee(dai.address, { rawValue: finalFee });
1932

2033
// Deploy the hubPool
21-
const merkleLib = await (await getContractFactory("MerkleLib", deployerWallet)).deploy();
34+
const merkleLib = await (await getContractFactory("MerkleLib", signer)).deploy();
2235
const hubPool = await (
23-
await getContractFactory("HubPool", { signer: deployerWallet, libraries: { MerkleLib: merkleLib.address } })
24-
).deploy(bondAmount, refundProposalLiveness, weth.address, weth.address, timer.address);
36+
await getContractFactory("HubPool", { signer: signer, libraries: { MerkleLib: merkleLib.address } })
37+
).deploy(
38+
bondAmount,
39+
refundProposalLiveness,
40+
parentFixtureOutput.finder.address,
41+
identifier,
42+
weth.address,
43+
weth.address,
44+
parentFixtureOutput.timer.address
45+
);
2546

26-
return { timer, weth, usdc, dai, hubPool };
47+
return { weth, usdc, dai, hubPool, ...parentFixtureOutput };
2748
});
2849

2950
export async function enableTokensForLiquidityProvision(owner: Signer, hubPool: Contract, tokens: Contract[]) {

test/HubPool.RelayerRefund.ts

Lines changed: 104 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,136 @@
11
import { expect } from "chai";
22
import { Contract } from "ethers";
3-
43
import { ethers } from "hardhat";
5-
import { ZERO_ADDRESS } from "@uma/common";
4+
import { ZERO_ADDRESS, parseAncillaryData } from "@uma/common";
65
import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils";
7-
import { depositDestinationChainId, bondAmount, refundProposalLiveness } from "./constants";
8-
import { hubPoolFixture } from "./HubPool.Fixture";
6+
import * as consts from "./constants";
7+
import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture";
8+
9+
let hubPool: Contract, weth: Contract, optimisticOracle: Contract;
10+
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;
911

10-
let hubPool: Contract, weth: Contract, usdc: Contract;
11-
let owner: SignerWithAddress, dataWorker: SignerWithAddress;
12+
const mockBundleEvaluationBlockNumbers = [1, 2, 3];
13+
const mockPoolRebalanceLeafCount = 5;
14+
const mockPoolRebalanceRoot = createRandomBytes32();
15+
const mockDestinationDistributionRoot = createRandomBytes32();
1216

1317
describe("HubPool Relayer Refund", function () {
14-
before(async function () {
15-
[owner, dataWorker] = await ethers.getSigners();
16-
({ weth, hubPool, usdc } = await hubPoolFixture());
17-
await seedWallet(dataWorker, [], weth, bondAmount);
18+
beforeEach(async function () {
19+
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
20+
({ weth, hubPool, optimisticOracle } = await hubPoolFixture());
21+
await seedWallet(dataWorker, [], weth, consts.bondAmount);
22+
await seedWallet(owner, [], weth, consts.bondAmount);
23+
await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2));
24+
await seedWallet(liquidityProvider, [], weth, consts.amountToLp);
25+
26+
await enableTokensForLiquidityProvision(owner, hubPool, [weth]);
27+
await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp);
28+
await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp);
1829
});
1930

2031
it("Initialization of a relay correctly stores data, emits events and pulls the bond", async function () {
21-
const bundleEvaluationBlockNumbers = [1, 2, 3];
22-
const poolRebalanceLeafCount = 5;
23-
const poolRebalanceProof = createRandomBytes32();
24-
const destinationDistributionProof = createRandomBytes32();
32+
const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness;
33+
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount);
34+
const dataWorkerWethBalancerBefore = await weth.callStatic.balanceOf(dataWorker.address);
2535

26-
const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + refundProposalLiveness;
27-
await weth.connect(dataWorker).approve(hubPool.address, bondAmount);
2836
await expect(
2937
hubPool
3038
.connect(dataWorker)
3139
.initiateRelayerRefund(
32-
bundleEvaluationBlockNumbers,
33-
poolRebalanceLeafCount,
34-
poolRebalanceProof,
35-
destinationDistributionProof
40+
mockBundleEvaluationBlockNumbers,
41+
mockPoolRebalanceLeafCount,
42+
mockPoolRebalanceRoot,
43+
mockDestinationDistributionRoot
3644
)
3745
)
3846
.to.emit(hubPool, "InitiateRefundRequested")
3947
.withArgs(
40-
0,
4148
expectedRequestExpirationTimestamp,
42-
poolRebalanceLeafCount,
43-
bundleEvaluationBlockNumbers,
44-
poolRebalanceProof,
45-
destinationDistributionProof,
49+
mockPoolRebalanceLeafCount,
50+
mockBundleEvaluationBlockNumbers,
51+
mockPoolRebalanceRoot,
52+
mockDestinationDistributionRoot,
4653
dataWorker.address
4754
);
4855
// Balances of the hubPool should have incremented by the bond and the dataWorker should have decremented by the bond.
49-
expect(await weth.balanceOf(hubPool.address)).to.equal(bondAmount);
50-
expect(await weth.balanceOf(dataWorker.address)).to.equal(0);
56+
expect(await weth.balanceOf(hubPool.address)).to.equal(consts.bondAmount.add(consts.amountToLp));
57+
expect(await weth.balanceOf(dataWorker.address)).to.equal(dataWorkerWethBalancerBefore.sub(consts.bondAmount));
58+
59+
const refundRequest = await hubPool.refundRequest();
60+
expect(refundRequest.requestExpirationTimestamp).to.equal(expectedRequestExpirationTimestamp);
61+
expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount);
62+
expect(refundRequest.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot);
63+
expect(refundRequest.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot);
64+
expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0.
65+
expect(refundRequest.proposer).to.equal(dataWorker.address);
66+
expect(refundRequest.proposerBondRepaid).to.equal(false);
5167

5268
// Can not re-initialize if the previous bundle has unclaimed leaves.
5369
await expect(
5470
hubPool
5571
.connect(dataWorker)
5672
.initiateRelayerRefund(
57-
bundleEvaluationBlockNumbers,
58-
poolRebalanceLeafCount,
59-
poolRebalanceProof,
60-
destinationDistributionProof
73+
mockBundleEvaluationBlockNumbers,
74+
mockPoolRebalanceLeafCount,
75+
mockPoolRebalanceRoot,
76+
mockDestinationDistributionRoot
6177
)
62-
).to.be.revertedWith("Last bundle has unclaimed leafs");
78+
).to.be.revertedWith("Active request has unclaimed leafs");
79+
});
80+
it("Dispute relayer refund correctly deletes the active request and enqueues a price request with the OO", async function () {
81+
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));
82+
await hubPool
83+
.connect(dataWorker)
84+
.initiateRelayerRefund(
85+
mockBundleEvaluationBlockNumbers,
86+
mockPoolRebalanceLeafCount,
87+
mockPoolRebalanceRoot,
88+
mockDestinationDistributionRoot
89+
);
90+
91+
const preCallAncillaryData = await hubPool._getRefundProposalAncillaryData();
92+
93+
await hubPool.connect(dataWorker).disputeRelayerRefund();
94+
95+
// Data should be deleted from the contracts refundRequest struct.
96+
const refundRequest = await hubPool.refundRequest();
97+
expect(refundRequest.requestExpirationTimestamp).to.equal(0);
98+
expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(0);
99+
expect(refundRequest.poolRebalanceRoot).to.equal(consts.zeroBytes32);
100+
expect(refundRequest.destinationDistributionRoot).to.equal(consts.zeroBytes32);
101+
expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0.
102+
expect(refundRequest.proposer).to.equal(consts.zeroAddress);
103+
expect(refundRequest.proposerBondRepaid).to.equal(false);
104+
105+
const priceProposalEvent = (await optimisticOracle.queryFilter(optimisticOracle.filters.ProposePrice()))[0].args;
106+
107+
expect(priceProposalEvent?.requester).to.equal(hubPool.address);
108+
expect(priceProposalEvent?.identifier).to.equal(consts.identifier);
109+
expect(priceProposalEvent?.ancillaryData).to.equal(preCallAncillaryData);
110+
111+
const parsedAncillaryData = parseAncillaryData(priceProposalEvent?.ancillaryData);
112+
expect(parsedAncillaryData?.requestExpirationTimestamp).to.equal(
113+
Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness
114+
);
115+
expect(parsedAncillaryData?.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount);
116+
expect("0x" + parsedAncillaryData?.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot);
117+
expect("0x" + parsedAncillaryData?.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot);
118+
expect(parsedAncillaryData?.claimedBitMap).to.equal(0);
119+
expect(ethers.utils.getAddress("0x" + parsedAncillaryData?.proposer)).to.equal(dataWorker.address);
120+
});
121+
it("Can not dispute after proposal liveness", async function () {
122+
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));
123+
await hubPool
124+
.connect(dataWorker)
125+
.initiateRelayerRefund(
126+
mockBundleEvaluationBlockNumbers,
127+
mockPoolRebalanceLeafCount,
128+
mockPoolRebalanceRoot,
129+
mockDestinationDistributionRoot
130+
);
131+
132+
await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness + 1);
133+
134+
await expect(hubPool.connect(dataWorker).disputeRelayerRefund()).to.be.revertedWith("Request passed liveness");
63135
});
64136
});

0 commit comments

Comments
 (0)