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

Contract pausing #164

Merged
merged 74 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
9042f78
Add Pausable helper contract
m-chrzan Sep 15, 2023
2f699e8
Add pause/unpause to Account
m-chrzan Sep 15, 2023
29c243a
Enabling pausing functions in Account
m-chrzan Sep 15, 2023
c54bb69
Add NatSpecs to Account
m-chrzan Sep 21, 2023
7cbe2da
Move isPaused logic to Pausable
m-chrzan Sep 21, 2023
4e4c883
Add NatSpecs to Pausable
m-chrzan Sep 21, 2023
7e1f8a0
Appease the linter
m-chrzan Sep 21, 2023
99e90f5
Update Account version
m-chrzan Sep 26, 2023
277a0d4
Create pauser with pause function
m-chrzan Oct 10, 2023
5e35998
Add unpause to Pauser
m-chrzan Oct 10, 2023
9d503d1
Specify exact revert message
m-chrzan Oct 10, 2023
5b49679
Make Pausable an abstract contract with IPausable functions
m-chrzan Oct 10, 2023
70fccbf
Add isPaused to Pausable interface
m-chrzan Oct 10, 2023
0a8dd61
Change pause/unpause permissions in Account
m-chrzan Oct 10, 2023
90fbb41
Add Pauser to MultiSig
m-chrzan Oct 11, 2023
2ddda5b
Allow a MultiSig signer to pause protocol contracts
m-chrzan Oct 11, 2023
d445957
Fix tests failing due to wrong proposal index
m-chrzan Oct 11, 2023
09c08e5
Go through Pauser contract
m-chrzan Oct 12, 2023
1b01ee0
Allow Governance to unpause contracts
m-chrzan Oct 12, 2023
12d1a5c
Appease the linter
m-chrzan Oct 12, 2023
1261dab
Document with NatSpecs
m-chrzan Oct 12, 2023
521a4b6
Add Pauser interface
m-chrzan Oct 13, 2023
f9176a7
Fix test
m-chrzan Oct 13, 2023
18c12dd
Add missing awaits
m-chrzan Oct 18, 2023
3aafc89
Reset network before Pauser test
m-chrzan Oct 18, 2023
c321be4
Store pause and pauser in assembly-accessible slots
m-chrzan Oct 24, 2023
51e1c8c
Appease the linter
m-chrzan Oct 24, 2023
6e69a9f
Remove timeout on GroupHealth test
m-chrzan Oct 27, 2023
f6ddbb2
Emit events on pause/unpause
m-chrzan Oct 30, 2023
45560d4
Add PauserSet event
m-chrzan Oct 30, 2023
eae36ec
Test _setPauser
m-chrzan Oct 30, 2023
60115f7
Remove unnecessary import
m-chrzan Nov 16, 2023
401b2e9
Disable proposal submission when MultiSig paused
m-chrzan Dec 18, 2023
455e7b9
Disable confirmProposal when paused
m-chrzan Dec 18, 2023
969b9e5
Disable scheduling and executing when paused
m-chrzan Dec 18, 2023
51f10b0
Make setPauser onlyOwner
m-chrzan Dec 31, 2023
6e12ff9
Add event tests
m-chrzan Jan 2, 2024
39031b9
Use actual owner from test deployment
m-chrzan Jan 2, 2024
21fc013
Make DefaultStrategy pausable
m-chrzan Jan 4, 2024
71f93b6
Add Pausable to GroupHealth
m-chrzan Jan 10, 2024
af666c1
Use named owner account in Manager test
m-chrzan Jan 10, 2024
2bc81a8
Add Pausable to Manager
m-chrzan Jan 10, 2024
25dd619
Add Pausable to RebasedStakedCelo
m-chrzan Jan 10, 2024
db32949
Use named owner in SpecificGroupStrategy test
m-chrzan Jan 10, 2024
c4fec4b
Add Pausable to SpecificGroupStrategy
m-chrzan Jan 10, 2024
b926580
Add Pausable to StakedCelo
m-chrzan Jan 11, 2024
0eb8ef3
Make ERC20 functions on StakedCelo pausable
m-chrzan Jan 16, 2024
14d7485
Make rstCELO ERC20 functions pausable
m-chrzan Jan 16, 2024
88a82fd
Fix Manager tests
m-chrzan Jan 18, 2024
92ceeed
Fix DefaultStrategy test
m-chrzan Jan 18, 2024
3b13532
Appease the linter
m-chrzan Jan 18, 2024
15f4a6d
Merge branch 'master' into m-chrzan/protocol-pause
m-chrzan Jan 22, 2024
a26abae
Merge branch 'master' into m-chrzan/protocol-pause
m-chrzan Feb 1, 2024
7d9e699
Bump version numbers
m-chrzan Feb 1, 2024
78ae33d
Tooling fix
pahor167 Feb 2, 2024
3a35586
multisig version
pahor167 Feb 2, 2024
8b9831b
Don't use core deployment for Vote tests
m-chrzan Feb 4, 2024
b570b24
Make Vote pausable
m-chrzan Feb 4, 2024
e345bfe
Appease the linter
m-chrzan Feb 4, 2024
637606c
Add missing deploy script
m-chrzan Feb 5, 2024
6baafe2
Update version number
m-chrzan Feb 5, 2024
a293d97
Fix typo
m-chrzan Feb 7, 2024
e7b74bb
Rename variables to avoid shadowed name
m-chrzan Feb 7, 2024
fb1a5f4
Merge branch 'master' into m-chrzan/protocol-pause
m-chrzan Feb 14, 2024
1905e56
Remove .only
m-chrzan Feb 14, 2024
c6f28e9
Move Pauser functionality to MultiSig
m-chrzan Feb 14, 2024
62395a5
Move AddressNotZero to common errors contract
m-chrzan Feb 16, 2024
cb61e49
Do not allow address 0 pauser
m-chrzan Feb 16, 2024
f8d935a
Use common error name instead of NullAddress
m-chrzan Feb 16, 2024
1ae5ef8
Remove unused imports and variables
m-chrzan Feb 16, 2024
9486e71
Let Governance pause contracts
m-chrzan Feb 20, 2024
f153bc0
Always set pauser to owner
m-chrzan Feb 20, 2024
d97b07d
Appease the linter
m-chrzan Feb 20, 2024
edd5496
Add test
m-chrzan Feb 21, 2024
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
50 changes: 41 additions & 9 deletions contracts/Account.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import "./Managed.sol";
import "./common/UUPSOwnableUpgradeable.sol";
import "./common/UsingRegistryUpgradeable.sol";
import "./interfaces/IAccount.sol";
import "./Pausable.sol";
m-chrzan marked this conversation as resolved.
Show resolved Hide resolved

/**
* @title A contract that facilitates voting on behalf of StakedCelo.sol.
* @notice This contract depends on the Manager to decide how to distribute votes and how to
* keep track of ownership of CELO voted via this contract.
*/
contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, IAccount {
contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, IAccount, Pausable {
/**
* @notice Used to keep track of a pending withdrawal. A similar data structure
* exists within LockedGold.sol, but it only keeps track of pending withdrawals
Expand Down Expand Up @@ -64,6 +65,11 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
*/
uint256 public totalScheduledWithdrawals;

/**
* @notice Controls whether the contract is paused.
*/
PausedRecord public paused;

/**
* @notice Emitted when CELO is scheduled for voting for a given group.
* @param group The validator group the CELO is intended to vote for.
Expand Down Expand Up @@ -225,6 +231,23 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
}
}

/**
* @notice Pauses external access to the contract.
* @dev Functions with the `onlyWhenNotPaused` modifier will be paused. This
* should be all the non-permissioned (i.e. not `onlyOwner` or * `onlyManager`)
* external/public functions.
*/
function pause() external onlyOwner {
_pause(paused);
}

/**
* @notice Unpauses the contract if it was previously paused using `pause()`.
*/
function unpause() external onlyOwner {
m-chrzan marked this conversation as resolved.
Show resolved Hide resolved
_unpause(paused);
}

/**
* @notice Deposits CELO sent via msg.value as unlocked CELO intended as
* votes for groups.
Expand All @@ -238,6 +261,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
external
payable
onlyManager
onlyWhenNotPaused(paused)
{
if (groups.length != votes.length) {
revert GroupsAndVotesArrayLengthsMismatch();
Expand Down Expand Up @@ -268,7 +292,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
uint256[] calldata fromVotes,
address[] calldata toGroups,
uint256[] calldata toVotes
) external onlyManager {
) external onlyManager onlyWhenNotPaused(paused) {
if (fromGroups.length != fromVotes.length || toGroups.length != toVotes.length) {
revert GroupsAndVotesArrayLengthsMismatch();
}
Expand Down Expand Up @@ -301,7 +325,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
address beneficiary,
address[] calldata groups,
uint256[] calldata withdrawals
) external onlyManager {
) external onlyManager onlyWhenNotPaused(paused) {
if (groups.length != withdrawals.length) {
revert GroupsAndVotesArrayLengthsMismatch();
}
Expand Down Expand Up @@ -355,7 +379,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
address lesserAfterActiveRevoke,
address greaterAfterActiveRevoke,
uint256 index
) external returns (uint256) {
) external onlyWhenNotPaused(paused) returns (uint256) {
uint256 withdrawalAmount = scheduledVotes[group].toWithdrawFor[beneficiary];
if (withdrawalAmount == 0) {
revert NoScheduledWithdrawal(beneficiary, group);
Expand Down Expand Up @@ -431,7 +455,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
address group,
address voteLesser,
address voteGreater
) external {
) external onlyWhenNotPaused(paused) {
IElection election = getElection();

// The amount of unlocked CELO for group that we want to lock and vote with.
Expand Down Expand Up @@ -491,7 +515,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
address beneficiary,
uint256 localPendingWithdrawalIndex,
uint256 lockedGoldPendingWithdrawalIndex
) external returns (uint256 amount) {
) external onlyWhenNotPaused(paused) returns (uint256 amount) {
(uint256 value, uint256 timestamp) = validatePendingWithdrawalRequest(
beneficiary,
localPendingWithdrawalIndex,
Expand Down Expand Up @@ -544,7 +568,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
uint256 yesVotes,
uint256 noVotes,
uint256 abstainVotes
) external onlyManager {
) external onlyManager onlyWhenNotPaused(paused) {
bool voteResult = getGovernance().votePartially(
proposalId,
index,
Expand Down Expand Up @@ -680,6 +704,14 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
return scheduledVotes[group].toWithdrawFor[beneficiary];
}

/**
* @notice Returns the paused status of the contract.
* @return `true` if the contract is paused, `false` otherwise.
*/
function isPaused() external view returns (bool) {
return _isPaused(paused);
}

/**
* @notice Returns the storage, major, minor, and patch version of the contract.
* @return Storage version of the contract.
Expand All @@ -697,7 +729,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
uint256
)
{
return (1, 2, 0, 0);
return (1, 2, 1, 0);
}

/**
Expand Down Expand Up @@ -728,7 +760,7 @@ contract Account is UUPSOwnableUpgradeable, UsingRegistryUpgradeable, Managed, I
address lesserAfterActiveRevoke,
address greaterAfterActiveRevoke,
uint256 index
) public {
) public onlyWhenNotPaused(paused) {
(, uint256 revokeAmount) = getAndUpdateToVoteAndToRevoke(group, 0, 0);

if (revokeAmount == 0) {
Expand Down
68 changes: 68 additions & 0 deletions contracts/Pausable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

/**
* @title A helper contract to add pasuing functionality to a contract.
* @notice Used to prevent/mitigate damage in case an exploit is found in the
* extending contract.
*/
contract Pausable {
/**
* @notice Struct wrapping a bool that determines whether the contract
* should be paused.
* @dev We can't store the flag in this contract itself, since that would
* mangle storage of inheriting contracts. Instead, we provide a
* struct-wrapped bool type, to leverage the typechecker to help ensure the
* correct value is used.
*/
struct PausedRecord {
bool paused;
}

/**
* @notice Used when an `onlyWhenNotPaused` function is called while the
* contract is paused.
*/
error Paused();

/**
* @notice Reverts if the contract is paused.
* @param paused The `PausedRecord` struct containing the flag.
*/
modifier onlyWhenNotPaused(PausedRecord storage paused) {
if (paused.paused) {
revert Paused();
}

_;
}

/**
* @notice Pauses the contract by setting the wrapped bool to `true`.
* @param paused The PausedRecord to modify.
* @dev The implementing contract should likely wrap this function in a
* permissioned (e.g. `onlyOwner`) `pause()` function.
*/
function _pause(PausedRecord storage paused) internal {
paused.paused = true;
}

/**
* @notice Unpauses the contract by setting the wrapped bool to `false`.
* @param paused The PausedRecord to modify.
* @dev The implementing contract should likely wrap this function in a
* permissioned (e.g. `onlyOwner`) `unpause()` function.
*/
function _unpause(PausedRecord storage paused) internal {
paused.paused = false;
}

/**
* @notice Returns whether or not the contract is paused.
* @param paused The PauseRecord to check.
* @return `true` if the contract is paused, `false` otherwise.
*/
function _isPaused(PausedRecord storage paused) internal view returns (bool) {
return paused.paused;
}
}
29 changes: 29 additions & 0 deletions contracts/test/PausableTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//SPDX-License-Identifier: LGPL-3.0-only
pragma solidity 0.8.11;

import "../Pausable.sol";

contract PausableTest is Pausable {
PausedRecord paused;
uint256 public numberCalls;

function callPausable() external onlyWhenNotPaused(paused) {
numberCalls++;
}

function callAlways() external {
numberCalls++;
}

function pause() external {
_pause(paused);
}

function unpause() external {
_unpause(paused);
}

function isPaused() external view returns (bool) {
return _isPaused(paused);
}
}
15 changes: 15 additions & 0 deletions deploy/test/pausable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DeployFunction } from "@celo/staked-celo-hardhat-deploy/types";
import { HardhatRuntimeEnvironment } from "hardhat/types";

const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const { deploy } = hre.deployments;
const { deployer, owner } = await hre.getNamedAccounts();
await deploy("PausableTest", {
from: deployer,
log: true,
});
};

func.id = "deploy_test_pausable";
func.tags = ["TestPausable"];
export default func;
112 changes: 111 additions & 1 deletion test-ts/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe("Account", () => {

let account: Account;

let owner: SignerWithAddress;
let manager: SignerWithAddress;
let nonManager: SignerWithAddress;
let beneficiary: SignerWithAddress;
Expand Down Expand Up @@ -84,7 +85,7 @@ describe("Account", () => {

beforeEach(async () => {
await hre.deployments.fixture("TestAccount");
const owner = await hre.ethers.getNamedSigner("owner");
owner = await hre.ethers.getNamedSigner("owner");
account = await hre.ethers.getContract("Account");
await account.connect(owner).setManager(manager.address);
governance = await hre.ethers.getContract("MockGovernance");
Expand Down Expand Up @@ -2015,4 +2016,113 @@ describe("Account", () => {
expect(await governance.abstainVotes()).to.eq(abstain);
});
});

describe("#pause", () => {
soloseng marked this conversation as resolved.
Show resolved Hide resolved
it("can be called by the owner", async () => {
await account.connect(owner).pause();
const isPaused = await account.isPaused();
expect(isPaused).to.be.true;
});

it("cannot be called by a random account", async () => {
await expect(account.connect(beneficiary).pause()).revertedWith(
"Ownable: caller is not the owner"
);
const isPaused = await account.isPaused();
expect(isPaused).to.be.false;
});
});

describe("#unpause", () => {
beforeEach(async () => {
await account.connect(owner).pause();
});

it("can be called by the owner", async () => {
await account.connect(owner).unpause();
const isPaused = await account.isPaused();
expect(isPaused).to.be.false;
});

it("cannot be called by a random account", async () => {
await expect(account.connect(beneficiary).unpause()).revertedWith(
"Ownable: caller is not the owner"
);
const isPaused = await account.isPaused();
expect(isPaused).to.be.true;
});
});

describe("when paused", () => {
beforeEach(async () => {
await account.connect(owner).pause();
});

it("can't call scheduleVotes", async () => {
await expect(
account.connect(manager).scheduleVotes([groupAddresses[0]], [100], { value: "100" })
).revertedWith("Paused()");
});

it("can't call scheduleTransfer", async () => {
await expect(
account
.connect(manager)
.scheduleTransfer([groupAddresses[0]], [1], [groupAddresses[1]], [1])
).revertedWith("Paused()");
});

it("can't call scheduleWithdrawals", async () => {
await expect(
account
.connect(manager)
.scheduleWithdrawals(beneficiary.address, [groupAddresses[0]], [260])
).revertedWith("Paused()");
});

it("can't call withdraw", async () => {
await expect(
account
.connect(manager)
.withdraw(
beneficiary.address,
groupAddresses[0],
ADDRESS_ZERO,
ADDRESS_ZERO,
ADDRESS_ZERO,
ADDRESS_ZERO,
0
)
).revertedWith("Paused()");
});

it("can't call activateAndVote", async () => {
await expect(
account.activateAndVote(groupAddresses[0], groupAddresses[1], ADDRESS_ZERO)
).revertedWith("Paused()");
});

it("can't call finishPendingWithdrawal", async () => {
await expect(account.finishPendingWithdrawal(beneficiary.address, 0, 0)).revertedWith(
"Paused()"
);
});

it("can't call votePartially", async () => {
await expect(account.connect(manager).votePartially(0, 0, 1, 1, 1)).revertedWith("Paused()");
});

it("can't call revokeVotes", async () => {
await expect(
account.revokeVotes(
groupAddresses[0],
ADDRESS_ZERO,
ADDRESS_ZERO,
ADDRESS_ZERO,
ADDRESS_ZERO,
0
)
).revertedWith("Paused()");
});
});
});
Loading
Loading