Skip to content
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

feat: Enable pausing deposits and fills unilaterally #212

Merged
merged 4 commits into from
Dec 30, 2022
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
39 changes: 36 additions & 3 deletions contracts/SpokePool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
// Count of deposits is used to construct a unique deposit identifier for this spoke pool.
uint32 public numberOfDeposits;

// Whether deposits and fills are disabled.
bool public pausedFills;
bool public pausedDeposits;

// This contract can store as many root bundles as the HubPool chooses to publish here.
RootBundle[] public rootBundles;

Expand Down Expand Up @@ -121,6 +125,8 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
address caller
);
event EmergencyDeleteRootBundle(uint256 indexed rootBundleId);
event PausedDeposits(bool isPaused);
event PausedFills(bool isPaused);

/**
* @notice Construct the base SpokePool.
Expand Down Expand Up @@ -151,10 +157,35 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
_;
}

modifier unpausedDeposits() {
require(!pausedDeposits, "Paused deposits");
_;
}

modifier unpausedFills() {
require(!pausedFills, "Paused fills");
_;
}

/**************************************
* ADMIN FUNCTIONS *
**************************************/

/**
* @notice Pauses deposit and fill functions. This is intended to be used during upgrades or when
* something goes awry.
* @param pause true if the call is meant to pause the system, false if the call is meant to unpause it.
*/
function pauseDeposits(bool pause) public override onlyAdmin nonReentrant {
pausedDeposits = pause;
emit PausedDeposits(pause);
}

function pauseFills(bool pause) public override onlyAdmin nonReentrant {
pausedFills = pause;
emit PausedFills(pause);
}

/**
* @notice Change cross domain admin address. Callable by admin only.
* @param newCrossDomainAdmin New cross domain admin.
Expand Down Expand Up @@ -250,7 +281,7 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
uint256 destinationChainId,
uint64 relayerFeePct,
uint32 quoteTimestamp
) public payable override nonReentrant {
) public payable override nonReentrant unpausedDeposits {
// Check that deposit route is enabled.
require(enabledDepositRoutes[originToken][destinationChainId], "Disabled route");

Expand Down Expand Up @@ -303,6 +334,8 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
* @notice This function will revert if the depositor did not sign a message containing the updated fee for the
* deposit ID stored in this contract. If the deposit ID is for another contract, or the depositor address is
* incorrect, or the updated fee is incorrect, then the signature will not match and this function will revert.
* @notice This function is not subject to a deposit pause on the off chance that deposits sent before all deposits
* are paused have very low fees and the user wants to entice a relayer to fill them with a higher fee.
* @param depositor Signer of the update fee message who originally submitted the deposit. If the deposit doesn't
* exist, then the relayer will not be able to fill any relay, so the caller should validate that the depositor
* did in fact submit a relay.
Expand Down Expand Up @@ -367,7 +400,7 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
uint64 realizedLpFeePct,
uint64 relayerFeePct,
uint32 depositId
) public nonReentrant {
) public nonReentrant unpausedFills {
// Each relay attempt is mapped to the hash of data uniquely identifying it, which includes the deposit data
// such as the origin chain ID and the deposit ID, and the data in a relay attempt such as who the recipient
// is, which chain and currency the recipient wants to receive funds on, and the relay fees.
Expand Down Expand Up @@ -423,7 +456,7 @@ abstract contract SpokePool is SpokePoolInterface, Testable, Lockable, MultiCall
uint64 newRelayerFeePct,
uint32 depositId,
bytes memory depositorSignature
) public override nonReentrant {
) public override nonReentrant unpausedFills {
_verifyUpdateRelayerFeeMessage(depositor, originChainId, newRelayerFeePct, depositId, depositorSignature);

// Now follow the default fillRelay flow with the updated fee and the original relay hash.
Expand Down
4 changes: 4 additions & 0 deletions contracts/SpokePoolInterface.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ interface SpokePoolInterface {
bool enable
) external;

function pauseDeposits(bool pause) external;

function pauseFills(bool pause) external;

function setDepositQuoteTimeBuffer(uint32 buffer) external;

function relayRootBundle(bytes32 relayerRefundRoot, bytes32 slowRelayRoot) external;
Expand Down
16 changes: 16 additions & 0 deletions test/SpokePool.Admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ describe("SpokePool Admin Functions", async function () {
expect(await spokePool.depositQuoteTimeBuffer()).to.equal(60);
});

it("Pause deposits", async function () {
expect(await spokePool.pausedDeposits()).to.equal(false);
await expect(spokePool.connect(owner).pauseDeposits(true)).to.emit(spokePool, "PausedDeposits").withArgs(true);
expect(await spokePool.pausedDeposits()).to.equal(true);
await expect(spokePool.connect(owner).pauseDeposits(false)).to.emit(spokePool, "PausedDeposits").withArgs(false);
expect(await spokePool.pausedDeposits()).to.equal(false);
});

it("Pause fills", async function () {
expect(await spokePool.pausedFills()).to.equal(false);
await expect(spokePool.connect(owner).pauseFills(true)).to.emit(spokePool, "PausedFills").withArgs(true);
expect(await spokePool.pausedFills()).to.equal(true);
await expect(spokePool.connect(owner).pauseFills(false)).to.emit(spokePool, "PausedFills").withArgs(false);
expect(await spokePool.pausedFills()).to.equal(false);
});

it("Delete rootBundle", async function () {
await spokePool.connect(owner).relayRootBundle(mockRelayerRefundRoot, mockSlowRelayRoot);

Expand Down
19 changes: 19 additions & 0 deletions test/SpokePool.Deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,25 @@ describe("SpokePool Depositor Logic", async function () {
});
it("Depositing ERC20 tokens correctly pulls tokens and changes contract state", async function () {
const currentSpokePoolTime = await spokePool.getCurrentTime();

// Can't deposit when paused:
await spokePool.connect(depositor).pauseDeposits(true);
await expect(
spokePool
.connect(depositor)
.deposit(
...getDepositParams(
recipient.address,
erc20.address,
amountToDeposit,
destinationChainId,
depositRelayerFeePct,
currentSpokePoolTime
)
)
).to.be.reverted;
await spokePool.connect(depositor).pauseDeposits(false);

await expect(
spokePool
.connect(depositor)
Expand Down
6 changes: 6 additions & 0 deletions test/SpokePool.Relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ describe("SpokePool Relayer Logic", async function () {
destErc20.address
);

// Can't fill when paused:
await spokePool.connect(depositor).pauseFills(true);
await expect(spokePool.connect(relayer).fillRelay(...getFillRelayParams(relayData, consts.amountToRelay))).to.be
.reverted;
await spokePool.connect(depositor).pauseFills(false);

await expect(spokePool.connect(relayer).fillRelay(...getFillRelayParams(relayData, consts.amountToRelay)))
.to.emit(spokePool, "FilledRelay")
.withArgs(
Expand Down