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
13 changes: 0 additions & 13 deletions contracts/BridgeAdmin.sol

This file was deleted.

212 changes: 146 additions & 66 deletions contracts/HubPool.sol

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions contracts/MerkleLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ library MerkleLib {
* @notice Tests whether a claim is contained within a claimedBitMap mapping.
* @param claimedBitMap a simple uint256 mapping in storage used as a bitmap.
* @param index the index to check in the bitmap.
* @return bool indicating if the index within the claimedBitMap has been marked as claimed.
*/
function isClaimed(mapping(uint256 => uint256) storage claimedBitMap, uint256 index) public view returns (bool) {
uint256 claimedWordIndex = index / 256;
Expand All @@ -105,4 +106,25 @@ library MerkleLib {
uint256 claimedBitIndex = index % 256;
claimedBitMap[claimedWordIndex] = claimedBitMap[claimedWordIndex] | (1 << claimedBitIndex);
}

/**
* @notice Tests whether a claim is contained within a 1D claimedBitMap mapping.
* @param claimedBitMap a simple uint256 value, encoding a 1D bitmap.
* @param index the index to check in the bitmap.
\* @return bool indicating if the index within the claimedBitMap has been marked as claimed.
*/
function isClaimed1D(uint256 claimedBitMap, uint256 index) public pure returns (bool) {
Copy link
Member Author

Choose a reason for hiding this comment

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

these are the new merkleLib methods I added. v simple and adapted from the original ones to take in a uint256 claimedBitMap rather than mapping(uint256 => uint256) storage claimedBitMap

uint256 mask = (1 << index);
return claimedBitMap & mask == mask;
}

/**
* @notice Marks an index in a claimedBitMap as claimed.
* @param claimedBitMap a simple uint256 mapping in storage used as a bitmap.
* @param index the index to mark in the bitmap.
*/
function setClaimed1D(uint256 claimedBitMap, uint256 index) public pure returns (uint256) {
require(index <= 255, "Index out of bounds");
return claimedBitMap | (1 << index % 256);
}
}
6 changes: 0 additions & 6 deletions contracts/interfaces/BridgeAdminInterface.sol

This file was deleted.

10 changes: 10 additions & 0 deletions contracts/test/MerkleLibTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import "../MerkleLib.sol";
contract MerkleLibTest {
mapping(uint256 => uint256) public claimedBitMap;

uint256 public claimedBitMap1D;

function verifyPoolRebalance(
bytes32 root,
MerkleLib.PoolRebalance memory rebalance,
Expand All @@ -32,4 +34,12 @@ contract MerkleLibTest {
function setClaimed(uint256 index) public {
MerkleLib.setClaimed(claimedBitMap, index);
}

function isClaimed1D(uint256 index) public view returns (bool) {
return MerkleLib.isClaimed1D(claimedBitMap1D, index);
}

function setClaimed1D(uint256 index) public {
claimedBitMap1D = MerkleLib.setClaimed1D(claimedBitMap1D, index);
}
}
23 changes: 20 additions & 3 deletions test/HubPool.Admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { expect } from "chai";
import { Contract } from "ethers";
import { ethers } from "hardhat";
import { ZERO_ADDRESS } from "@uma/common";
import { getContractFactory, SignerWithAddress } from "./utils";
import { depositDestinationChainId } from "./constants";
import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils";
import { depositDestinationChainId, bondAmount } from "./constants";
import { hubPoolFixture } from "./HubPool.Fixture";

let hubPool: Contract, weth: Contract, usdc: Contract;
let owner: SignerWithAddress, other: SignerWithAddress;

describe("HubPool Admin functions", function () {
before(async function () {
beforeEach(async function () {
[owner, other] = await ethers.getSigners();
({ weth, hubPool, usdc } = await hubPoolFixture());
});
Expand Down Expand Up @@ -43,4 +43,21 @@ describe("HubPool Admin functions", function () {
.withArgs(weth.address, depositDestinationChainId, usdc.address);
expect(await hubPool.whitelistedRoutes(weth.address, depositDestinationChainId)).to.equal(usdc.address);
});

it("Can change the bond token and amount", async function () {
expect(await hubPool.callStatic.bondToken()).to.equal(weth.address); // Default set in the fixture.
expect(await hubPool.callStatic.bondAmount()).to.equal(bondAmount); // Default set in the fixture.

// Set the bond token and amount to 1000 USDC
const newBondAmount = ethers.utils.parseUnits("1000", 6); // set to 1000e6, i.e 1000 USDC.
await hubPool.setBond(usdc.address, newBondAmount);
expect(await hubPool.callStatic.bondToken()).to.equal(usdc.address); // New Address.
expect(await hubPool.callStatic.bondAmount()).to.equal(newBondAmount); // New Bond amount.
});
it("Can not change the bond token and amount during a pending refund", async function () {
await seedWallet(owner, [], weth, bondAmount);
await weth.approve(hubPool.address, bondAmount);
await hubPool.initiateRelayerRefund([1, 2, 3], 5, createRandomBytes32(), createRandomBytes32());
await expect(hubPool.setBond(usdc.address, "1")).to.be.revertedWith("Active request has unclaimed leafs");
});
});
51 changes: 36 additions & 15 deletions test/HubPool.Fixture.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
import { TokenRolesEnum } from "@uma/common";
import { getContractFactory } from "./utils";
import { bondAmount, refundProposalLiveness } from "./constants";
import { TokenRolesEnum, interfaceName } from "@uma/common";
import { getContractFactory, utf8ToHex, toBN, fromWei } from "./utils";
import { bondAmount, refundProposalLiveness, finalFee, identifier } from "./constants";
import { Contract, Signer } from "ethers";
import hre from "hardhat";

import { umaEcosystemFixture } from "./UmaEcosystem.Fixture";

export const hubPoolFixture = hre.deployments.createFixture(async ({ ethers }) => {
const [deployerWallet] = await ethers.getSigners();
const [signer] = await ethers.getSigners();

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

// Create 3 tokens: WETH for wrapping unwrapping and 2 ERC20s with different decimals.
const weth = await (await getContractFactory("WETH9", deployerWallet)).deploy();
const usdc = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("USD Coin", "USDC", 6);
await usdc.addMember(TokenRolesEnum.MINTER, deployerWallet.address);
const dai = await (await getContractFactory("ExpandedERC20", deployerWallet)).deploy("DAI Stablecoin", "DAI", 18);
await dai.addMember(TokenRolesEnum.MINTER, deployerWallet.address);
const weth = await (await getContractFactory("WETH9", signer)).deploy();
const usdc = await (await getContractFactory("ExpandedERC20", signer)).deploy("USD Coin", "USDC", 6);
await usdc.addMember(TokenRolesEnum.MINTER, signer.address);
const dai = await (await getContractFactory("ExpandedERC20", signer)).deploy("DAI Stablecoin", "DAI", 18);
await dai.addMember(TokenRolesEnum.MINTER, signer.address);

// Set the above currencies as approved in the UMA collateralWhitelist.
await parentFixtureOutput.collateralWhitelist.addToWhitelist(weth.address);
await parentFixtureOutput.collateralWhitelist.addToWhitelist(usdc.address);
await parentFixtureOutput.collateralWhitelist.addToWhitelist(dai.address);

// Set the finalFee for all the new tokens.
await parentFixtureOutput.store.setFinalFee(weth.address, { rawValue: finalFee });
await parentFixtureOutput.store.setFinalFee(usdc.address, { rawValue: toBN(fromWei(finalFee)).mul(1e6) });
await parentFixtureOutput.store.setFinalFee(dai.address, { rawValue: finalFee });

// Deploy the hubPool
const merkleLib = await (await getContractFactory("MerkleLib", deployerWallet)).deploy();
const merkleLib = await (await getContractFactory("MerkleLib", signer)).deploy();
const hubPool = await (
await getContractFactory("HubPool", { signer: deployerWallet, libraries: { MerkleLib: merkleLib.address } })
).deploy(bondAmount, refundProposalLiveness, weth.address, weth.address, timer.address);
await getContractFactory("HubPool", { signer: signer, libraries: { MerkleLib: merkleLib.address } })
).deploy(
bondAmount,
refundProposalLiveness,
parentFixtureOutput.finder.address,
identifier,
weth.address,
weth.address,
parentFixtureOutput.timer.address
);

return { timer, weth, usdc, dai, hubPool };
return { weth, usdc, dai, hubPool, ...parentFixtureOutput };
});

export async function enableTokensForLiquidityProvision(owner: Signer, hubPool: Contract, tokens: Contract[]) {
Expand Down
136 changes: 104 additions & 32 deletions test/HubPool.RelayerRefund.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,136 @@
import { expect } from "chai";
import { Contract } from "ethers";

import { ethers } from "hardhat";
import { ZERO_ADDRESS } from "@uma/common";
import { ZERO_ADDRESS, parseAncillaryData } from "@uma/common";
import { getContractFactory, SignerWithAddress, createRandomBytes32, seedWallet } from "./utils";
import { depositDestinationChainId, bondAmount, refundProposalLiveness } from "./constants";
import { hubPoolFixture } from "./HubPool.Fixture";
import * as consts from "./constants";
import { hubPoolFixture, enableTokensForLiquidityProvision } from "./HubPool.Fixture";

let hubPool: Contract, weth: Contract, optimisticOracle: Contract;
let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress;

let hubPool: Contract, weth: Contract, usdc: Contract;
let owner: SignerWithAddress, dataWorker: SignerWithAddress;
const mockBundleEvaluationBlockNumbers = [1, 2, 3];
const mockPoolRebalanceLeafCount = 5;
const mockPoolRebalanceRoot = createRandomBytes32();
const mockDestinationDistributionRoot = createRandomBytes32();

describe("HubPool Relayer Refund", function () {
before(async function () {
[owner, dataWorker] = await ethers.getSigners();
({ weth, hubPool, usdc } = await hubPoolFixture());
await seedWallet(dataWorker, [], weth, bondAmount);
beforeEach(async function () {
[owner, dataWorker, liquidityProvider] = await ethers.getSigners();
({ weth, hubPool, optimisticOracle } = await hubPoolFixture());
await seedWallet(dataWorker, [], weth, consts.bondAmount);
await seedWallet(owner, [], weth, consts.bondAmount);
await seedWallet(dataWorker, [], weth, consts.bondAmount.add(consts.finalFee).mul(2));
await seedWallet(liquidityProvider, [], weth, consts.amountToLp);

await enableTokensForLiquidityProvision(owner, hubPool, [weth]);
await weth.connect(liquidityProvider).approve(hubPool.address, consts.amountToLp);
await hubPool.connect(liquidityProvider).addLiquidity(weth.address, consts.amountToLp);
});

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

const expectedRequestExpirationTimestamp = Number(await hubPool.getCurrentTime()) + refundProposalLiveness;
await weth.connect(dataWorker).approve(hubPool.address, bondAmount);
await expect(
hubPool
.connect(dataWorker)
.initiateRelayerRefund(
bundleEvaluationBlockNumbers,
poolRebalanceLeafCount,
poolRebalanceProof,
destinationDistributionProof
mockBundleEvaluationBlockNumbers,
mockPoolRebalanceLeafCount,
mockPoolRebalanceRoot,
mockDestinationDistributionRoot
)
)
.to.emit(hubPool, "InitiateRefundRequested")
.withArgs(
0,
expectedRequestExpirationTimestamp,
poolRebalanceLeafCount,
bundleEvaluationBlockNumbers,
poolRebalanceProof,
destinationDistributionProof,
mockPoolRebalanceLeafCount,
mockBundleEvaluationBlockNumbers,
mockPoolRebalanceRoot,
mockDestinationDistributionRoot,
dataWorker.address
);
// Balances of the hubPool should have incremented by the bond and the dataWorker should have decremented by the bond.
expect(await weth.balanceOf(hubPool.address)).to.equal(bondAmount);
expect(await weth.balanceOf(dataWorker.address)).to.equal(0);
expect(await weth.balanceOf(hubPool.address)).to.equal(consts.bondAmount.add(consts.amountToLp));
expect(await weth.balanceOf(dataWorker.address)).to.equal(dataWorkerWethBalancerBefore.sub(consts.bondAmount));

const refundRequest = await hubPool.refundRequest();
expect(refundRequest.requestExpirationTimestamp).to.equal(expectedRequestExpirationTimestamp);
expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount);
expect(refundRequest.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot);
expect(refundRequest.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot);
expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0.
expect(refundRequest.proposer).to.equal(dataWorker.address);
expect(refundRequest.proposerBondRepaid).to.equal(false);

// Can not re-initialize if the previous bundle has unclaimed leaves.
await expect(
hubPool
.connect(dataWorker)
.initiateRelayerRefund(
bundleEvaluationBlockNumbers,
poolRebalanceLeafCount,
poolRebalanceProof,
destinationDistributionProof
mockBundleEvaluationBlockNumbers,
mockPoolRebalanceLeafCount,
mockPoolRebalanceRoot,
mockDestinationDistributionRoot
)
).to.be.revertedWith("Last bundle has unclaimed leafs");
).to.be.revertedWith("Active request has unclaimed leafs");
});
it("Dispute relayer refund correctly deletes the active request and enqueues a price request with the OO", async function () {
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));
await hubPool
.connect(dataWorker)
.initiateRelayerRefund(
mockBundleEvaluationBlockNumbers,
mockPoolRebalanceLeafCount,
mockPoolRebalanceRoot,
mockDestinationDistributionRoot
);

const preCallAncillaryData = await hubPool._getRefundProposalAncillaryData();

await hubPool.connect(dataWorker).disputeRelayerRefund();

// Data should be deleted from the contracts refundRequest struct.
const refundRequest = await hubPool.refundRequest();
expect(refundRequest.requestExpirationTimestamp).to.equal(0);
expect(refundRequest.unclaimedPoolRebalanceLeafCount).to.equal(0);
expect(refundRequest.poolRebalanceRoot).to.equal(consts.zeroBytes32);
expect(refundRequest.destinationDistributionRoot).to.equal(consts.zeroBytes32);
expect(refundRequest.claimedBitMap).to.equal(0); // no claims yet so everything should be marked at 0.
expect(refundRequest.proposer).to.equal(consts.zeroAddress);
expect(refundRequest.proposerBondRepaid).to.equal(false);

const priceProposalEvent = (await optimisticOracle.queryFilter(optimisticOracle.filters.ProposePrice()))[0].args;

expect(priceProposalEvent?.requester).to.equal(hubPool.address);
expect(priceProposalEvent?.identifier).to.equal(consts.identifier);
expect(priceProposalEvent?.ancillaryData).to.equal(preCallAncillaryData);

const parsedAncillaryData = parseAncillaryData(priceProposalEvent?.ancillaryData);
expect(parsedAncillaryData?.requestExpirationTimestamp).to.equal(
Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness
);
expect(parsedAncillaryData?.unclaimedPoolRebalanceLeafCount).to.equal(mockPoolRebalanceLeafCount);
expect("0x" + parsedAncillaryData?.poolRebalanceRoot).to.equal(mockPoolRebalanceRoot);
expect("0x" + parsedAncillaryData?.destinationDistributionRoot).to.equal(mockDestinationDistributionRoot);
expect(parsedAncillaryData?.claimedBitMap).to.equal(0);
expect(ethers.utils.getAddress("0x" + parsedAncillaryData?.proposer)).to.equal(dataWorker.address);
});
it("Can not dispute after proposal liveness", async function () {
await weth.connect(dataWorker).approve(hubPool.address, consts.bondAmount.mul(10));
await hubPool
.connect(dataWorker)
.initiateRelayerRefund(
mockBundleEvaluationBlockNumbers,
mockPoolRebalanceLeafCount,
mockPoolRebalanceRoot,
mockDestinationDistributionRoot
);

await hubPool.setCurrentTime(Number(await hubPool.getCurrentTime()) + consts.refundProposalLiveness + 1);

await expect(hubPool.connect(dataWorker).disputeRelayerRefund()).to.be.revertedWith("Request passed liveness");
});
});
Loading