Skip to content

Commit

Permalink
Add signers as an alternative to authorize protected functions (#5)
Browse files Browse the repository at this point in the history
* Added signers as an alternative to authorize protected functions

* Added a few checks in tests

* Cleaned up storage layout

* Updated NatSpec doc

* Accounted for the function arguments in the signature

* Added None to ProtectedFunction enum

* Fixed tests

* Added NatSpec documentation

* Added ability to pause/unpause bridge by signers

* Updated NatSpec documentation

* Updated NatSpec documentation

* Updated NatSpec documentation. Updated __Bridge_init function
  • Loading branch information
KyrylR authored Mar 20, 2024
1 parent 59f49a7 commit 25ca58d
Show file tree
Hide file tree
Showing 13 changed files with 740 additions and 116 deletions.
99 changes: 81 additions & 18 deletions contracts/bridge/Bridge.sol
Original file line number Diff line number Diff line change
@@ -1,38 +1,48 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

import {IBridge} from "../interfaces/bridge/IBridge.sol";

import {ERC20Handler} from "../handlers/ERC20Handler.sol";
import {ERC721Handler} from "../handlers/ERC721Handler.sol";
import {ERC1155Handler} from "../handlers/ERC1155Handler.sol";
import {NativeHandler} from "../handlers/NativeHandler.sol";

import {Hashes} from "../utils/Hashes.sol";
import {Signers} from "../utils/Signers.sol";
import {PauseManager} from "../utils/PauseManager.sol";
import {UUPSSignableUpgradeable} from "../utils/UUPSSignableUpgradeable.sol";

/**
* @title Bridge Contract
*/
contract Bridge is
IBridge,
UUPSUpgradeable,
UUPSSignableUpgradeable,
Signers,
Hashes,
PauseManager,
ERC20Handler,
ERC721Handler,
ERC1155Handler,
NativeHandler
{
/**
* @dev Ensures the function is callable only by the pause manager maintainer.
* @inheritdoc PauseManager
*/
modifier onlyPauseManagerMaintainer(bytes32 functionData_, bytes[] calldata signatures_)
override {
_checkOwnerOrSignatures(functionData_, signatures_);
_;
}

/**
* @inheritdoc PauseManager
*/
modifier onlyPauseManagerMaintainer() override {
_checkOwner();
modifier onlyPauseManager(bytes32 functionData_, bytes[] calldata signatures_) override {
if (pauseManager() != address(0)) {
_checkPauseManager();
} else {
_checkOwnerOrSignatures(functionData_, signatures_);
}
_;
}

Expand All @@ -52,19 +62,59 @@ contract Bridge is
_disableInitializers();
}

/**
* @notice Initializes the contract.
* @param signers_ The initial signers. Refer to the `Signers` contract for detailed limitations and information.
*
* @param pauseManager_ The address of the initial pause manager, which may be set to the zero address.
* When set to the zero address, the contract can be paused or unpaused by either the owner or the signers,
* depending on the `isSignersMode` flag.
*
* @param signaturesThreshold_ The number of signatures required to withdraw tokens or to execute a protected function.
* A list of all protected functions is available in the `IBridge` interface.
*
* @param isSignersMode_ The flag that enables or disables signers mode. When set to `true`,
* the contract requires signatures from the signers for executing a protected function.
*/
function __Bridge_init(
address[] calldata signers_,
uint256 signaturesThreshold_
address pauseManager_,
uint256 signaturesThreshold_,
bool isSignersMode_
) external initializer {
__Signers_init(signers_, signaturesThreshold_);
__Signers_init(signers_, signaturesThreshold_, isSignersMode_);

__PauseManager_init(owner());
__PauseManager_init(pauseManager_);
}

/**
/*
* @inheritdoc UUPSUpgradeable
*/
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
function _authorizeUpgrade(address) internal pure override {
revert("Bridge: this upgrade method is turned off");
}

/*
* @inheritdoc UUPSSignableUpgradeable
*
* @dev Depending on the `isSignersMode` flag in the `Signers` contract, this function requires
* either signatures from the signers or that the transaction be sent by the owner.
*
* | `isSignersMode` Flag | Callable By |
* |----------------------|---------------------------|
* | `false` | Owner |
* | `true` | Signers |
*/
function _authorizeUpgrade(
address newImplementation,
bytes[] calldata signatures_
) internal override {
bytes32 functionData_ = keccak256(
abi.encodePacked(IBridge.ProtectedFunction.BridgeUpgrade, newImplementation)
);

_checkOwnerOrSignatures(functionData_, signatures_);
}

/**
* @inheritdoc IBridge
Expand Down Expand Up @@ -182,13 +232,26 @@ contract Bridge is

/**
* @notice The function to add a new hash
* @param txHash_ The transaction hash from the other chain
* @param txNonce_ The nonce of the transaction from the other chain
* @param signatures_ The signatures of the signers; this field should be empty if the `isSignersMode` flag is set to ‘false’.
*
* @dev Depending on the `isSignersMode` flag in the `Signers` contract, this function requires
* either signatures from the signers or that the transaction be sent by the owner.
*
* | `isSignersMode` Flag | Callable By |
* |----------------------|---------------------------|
* | `false` | Owner |
* | `true` | Signers |
*/
function addHash(bytes32 txHash_, uint256 txNonce_) external onlyOwner {
_checkAndUpdateHashes(txHash_, txNonce_);
}
function addHash(bytes32 txHash_, uint256 txNonce_, bytes[] calldata signatures_) external {
bytes32 functionData_ = keccak256(
abi.encodePacked(IBridge.ProtectedFunction.AddHash, txHash_, txNonce_)
);

function _checkOwner() internal view {
require(owner() == _msgSender(), "Bridge: caller is not the owner");
_checkOwnerOrSignatures(functionData_, signatures_);

_checkAndUpdateHashes(txHash_, txNonce_);
}

function _checkNotStopped() internal view {
Expand Down
17 changes: 17 additions & 0 deletions contracts/interfaces/bridge/IBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ import {INativeHandler} from "../handlers/INativeHandler.sol";
* All signer addresses must differ in their first (most significant) 8 bits in order to pass a bloom filtering.
*/
interface IBridge is IERC20Handler, IERC721Handler, IERC1155Handler, INativeHandler {
/**
* @notice The enum of protected functions.
* Used as a domain separator for the sign hash when the signatures are required to execute a protected function.
*/
enum ProtectedFunction {
None,
Pause,
Unpause,
AddHash,
BridgeUpgrade,
SetPauseManager,
SetSignaturesThreshold,
AddSigners,
RemoveSigners,
ToggleSignersMode
}

/**
* @notice Withdraws ERC20 tokens.
* @param token_ The address of the token to withdraw.
Expand Down
20 changes: 17 additions & 3 deletions contracts/mocks/utils/PauseManagerMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@ pragma solidity ^0.8.9;

import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

import {IBridge} from "../../interfaces/bridge/IBridge.sol";

import {PauseManager} from "../../utils/PauseManager.sol";

contract PauseManagerMock is PauseManager, OwnableUpgradeable {
modifier onlyPauseManagerMaintainer() override {
modifier onlyPauseManagerMaintainer(bytes32 functionData_, bytes[] calldata signatures_)
override {
_checkOwner();
_;
}

modifier onlyPauseManager(bytes32 functionData_, bytes[] calldata signatures_) override {
_checkPauseManager();
_;
}

function __PauseManagerMock_init(address initialOwner_) public initializer {
__Ownable_init();

Expand All @@ -28,8 +36,14 @@ contract PauseManagerMock is PauseManager, OwnableUpgradeable {

contract PauseManagerMockCoverage is PauseManager {
function __PauseManagerMock_init(
address initialOwner_
) public initializer onlyPauseManagerMaintainer {
address initialOwner_,
bytes[] calldata signatures_
)
public
initializer
onlyPauseManager(bytes32(0), signatures_)
onlyPauseManagerMaintainer(bytes32(0), signatures_)
{
__PauseManager_init(initialOwner_);
}
}
5 changes: 3 additions & 2 deletions contracts/mocks/utils/SignersMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {Signers} from "../../utils/Signers.sol";
contract SignersMock is Signers {
function __SignersMock_init(
address[] calldata signers_,
uint256 signaturesThreshold_
uint256 signaturesThreshold_,
bool isSignersMode_
) public initializer {
__Signers_init(signers_, signaturesThreshold_);
__Signers_init(signers_, signaturesThreshold_, isSignersMode_);
}

function checkSignatures(bytes32 signHash_, bytes[] calldata signatures_) external view {
Expand Down
10 changes: 10 additions & 0 deletions contracts/utils/Hashes.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

/**
* @title Hashes
* @notice A contract that stores used hashes to prevent double spending
*/
abstract contract Hashes {
mapping(bytes32 => bool) public usedHashes; // keccak256(txHash . txNonce) => is used

Expand All @@ -11,6 +15,12 @@ abstract contract Hashes {
*/
uint256[49] private __gap;

/**
* @notice Check if the hash is used
* @param txHash_ The transaction hash from the other chain
* @param txNonce_ The nonce of the transaction from the other chain
* @return True if the hash is used, otherwise false
*/
function containsHash(bytes32 txHash_, uint256 txNonce_) external view returns (bool) {
bytes32 nonceHash_ = keccak256(abi.encodePacked(txHash_, txNonce_));

Expand Down
91 changes: 73 additions & 18 deletions contracts/utils/PauseManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ pragma solidity ^0.8.9;

import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol";

import {IBridge} from "../interfaces/bridge/IBridge.sol";

/**
* @title PauseManager Contract
* @notice Extends PausableUpgradeable from OpenZeppelin, extends existing functionality be allowing delegation of pause
* management to a specified address.
* @notice Extends PausableUpgradeable from OpenZeppelin by allowing the delegation of pause management
* to a specified address.
*/
abstract contract PauseManager is PausableUpgradeable {
address private _pauseManager;
Expand All @@ -25,17 +27,18 @@ abstract contract PauseManager is PausableUpgradeable {
event PauseManagerChanged(address indexed newManager);

/**
* @notice Modifier to make a function callable only by the pause manager maintainer.
* @notice Ensures the function is callable only by the pause manager maintainer(s).
*/
modifier onlyPauseManagerMaintainer() virtual {
modifier onlyPauseManagerMaintainer(bytes32 functionData_, bytes[] calldata signatures_)
virtual {
_;
}

/**
* @notice Modifier to make a function callable only by the current pause manager.
* @notice Ensures the function is callable only by the current pause manager.
* If the pause manager is not set, the function is callable by the owner or the signers.
*/
modifier onlyPauseManager() {
_checkPauseManager();
modifier onlyPauseManager(bytes32 functionData_, bytes[] calldata signatures_) virtual {
_;
}

Expand All @@ -44,38 +47,90 @@ abstract contract PauseManager is PausableUpgradeable {
* @param initialManager_ The address of the initial pause manager. Must not be the zero address.
*/
function __PauseManager_init(address initialManager_) internal onlyInitializing {
require(initialManager_ != address(0), "PauseManager: zero address");

__Pausable_init();

_setPauseManager(initialManager_);
}

/**
* @notice Pauses the contract.
* Can only be called by the current pause manager.
* @param signatures_ The signatures of the signers; this field should be empty
* if the `isSignersMode` flag is set to ‘false’ in the `Signers` contract or if the `pauseManager` is not the zero address.
*
* @dev Depending on the `isSignersMode` flag in the `Signers` contract, this function requires
* either signatures from the signers or that the transaction be sent by the owner.
*
* | `isSignersMode` Flag | `pauseManager` Address | Callable By |
* |----------------------|------------------------|---------------------------|
* | `false` | `address(0)` | Owner |
* | `false` | Not `address(0)` | Pause Manager |
* | `true` | `address(0)` | Signers |
* | `true` | Not `address(0)` | Pause Manager |
*/
function pause() public onlyPauseManager {
function pause(
bytes[] calldata signatures_
)
public
onlyPauseManager(keccak256(abi.encodePacked(IBridge.ProtectedFunction.Pause)), signatures_)
{
_pause();
}

/**
* @notice Unpauses the contract.
* Can only be called by the current pause manager.
* @param signatures_ The signatures of the signers; this field should be empty
* if the `isSignersMode` flag is set to ‘false’ in the `Signers` contract or if the `pauseManager` is not the zero address.
*
* @dev Depending on the `isSignersMode` flag in the `Signers` contract, this function requires
* either signatures from the signers or that the transaction be sent by the owner.
*
* | `isSignersMode` Flag | `pauseManager` Address | Callable By |
* |----------------------|------------------------|---------------------------|
* | `false` | `address(0)` | Owner |
* | `false` | Not `address(0)` | Pause Manager |
* | `true` | `address(0)` | Signers |
* | `true` | Not `address(0)` | Pause Manager |
*/
function unpause() public onlyPauseManager {
function unpause(
bytes[] calldata signatures_
)
public
onlyPauseManager(
keccak256(abi.encodePacked(IBridge.ProtectedFunction.Unpause)),
signatures_
)
{
_unpause();
}

/**
* @notice Transfers pause management to a new address.
* Can only be called by a pause manager maintainer.
* Can only be called by a pause manager maintainer(s).
*
* @param newManager_ The address of the new pause manager, which may be the zero address.
* When set to the zero address, the contract can be paused or unpaused by either the owner or the signers,
* depending on the `isSignersMode` flag.
*
* @param signatures_ The signatures of the signers; this field should be empty if the `isSignersMode` flag is set to ‘false’ in the `Signers` contract.
*
* @param newManager_ The address of the new pause manager. Must not be the zero address.
* @dev Depending on the `isSignersMode` flag in the `Signers` contract, this function requires
* either signatures from the signers or that the transaction be sent by the owner.
*
* | `isSignersMode` Flag | Callable By |
* |----------------------|---------------------------|
* | `false` | Owner |
* | `true` | Signers |
*/
function setPauseManager(address newManager_) public onlyPauseManagerMaintainer {
require(newManager_ != address(0), "PauseManager: zero address");

function setPauseManager(
address newManager_,
bytes[] calldata signatures_
)
public
onlyPauseManagerMaintainer(
keccak256(abi.encodePacked(IBridge.ProtectedFunction.SetPauseManager, newManager_)),
signatures_
)
{
_setPauseManager(newManager_);
}

Expand Down
Loading

0 comments on commit 25ca58d

Please sign in to comment.