diff --git a/protocol/governance/contracts/modules/core/GuardianModule.sol b/protocol/governance/contracts/modules/core/GuardianModule.sol new file mode 100644 index 0000000000..7f739f442b --- /dev/null +++ b/protocol/governance/contracts/modules/core/GuardianModule.sol @@ -0,0 +1,71 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {AccessError} from "@synthetixio/core-contracts/contracts/errors/AccessError.sol"; +import {AddressError} from "@synthetixio/core-contracts/contracts/errors/AddressError.sol"; +import {ChangeError} from "@synthetixio/core-contracts/contracts/errors/ChangeError.sol"; +import {Guardian} from "../../storage/Guardian.sol"; + +contract GuardianModule { + /** + * @notice Thrown when an address tries to accept guardian role but has not been nominated. + * @param addr The address that is trying to accept the guardian role. + */ + error NotNominated(address addr); + + /** + * @notice Emitted when an address has been nominated. + * @param newOwner The address that has been nominated. + */ + event GuardianNominated(address newOwner); + + /** + * @notice Emitted when the guardianship of the contract has changed. + * @param oldOwner The previous guardian of the contract. + * @param newOwner The new guardian of the contract. + */ + event GuardianChanged(address oldOwner, address newOwner); + + function acceptGuardianship() public { + Guardian.Data storage store = Guardian.load(); + + address currentNominatedGuardian = store.nominatedGuardian; + if (msg.sender != currentNominatedGuardian) { + revert NotNominated(msg.sender); + } + + emit GuardianChanged(store.guardian, currentNominatedGuardian); + + store.guardian = currentNominatedGuardian; + store.nominatedGuardian = address(0); + store.ownershipRequestedAt = 0; + } + + function nominateNewGuardian(address newNominatedGuardian) public { + Guardian.onlyGuardian(); + + Guardian.Data storage store = Guardian.load(); + + if (newNominatedGuardian == address(0)) { + revert AddressError.ZeroAddress(); + } + + if (newNominatedGuardian == store.nominatedGuardian) { + revert ChangeError.NoChange(); + } + + store.nominatedGuardian = newNominatedGuardian; + emit GuardianNominated(newNominatedGuardian); + } + + function renounceGuardianNomination() external { + Guardian.Data storage store = Guardian.load(); + + if (store.nominatedGuardian != msg.sender) { + revert NotNominated(msg.sender); + } + + store.nominatedGuardian = address(0); + store.ownershipRequestedAt = 0; + } +} diff --git a/protocol/governance/contracts/modules/core/OwnerModule.sol b/protocol/governance/contracts/modules/core/OwnerModule.sol index 1976b4ad6f..d17eb6f1fd 100644 --- a/protocol/governance/contracts/modules/core/OwnerModule.sol +++ b/protocol/governance/contracts/modules/core/OwnerModule.sol @@ -1,9 +1,61 @@ //SPDX-License-Identifier: MIT pragma solidity >=0.8.11 <0.9.0; +import {AccessError} from "@synthetixio/core-contracts/contracts/errors/AccessError.sol"; +import {AddressError} from "@synthetixio/core-contracts/contracts/errors/AddressError.sol"; +import {ChangeError} from "@synthetixio/core-contracts/contracts/errors/ChangeError.sol"; +import {SafeCastU256} from "@synthetixio/core-contracts/contracts/utils/SafeCast.sol"; import {OwnerModule as BaseOwnerModule} from "@synthetixio/core-modules/contracts/modules/OwnerModule.sol"; +import {OwnableStorage} from "@synthetixio/core-contracts/contracts/ownership/OwnableStorage.sol"; +import {Guardian} from "../../storage/Guardian.sol"; -// solhint-disable-next-line no-empty-blocks contract OwnerModule is BaseOwnerModule { + using SafeCastU256 for uint256; + /** + * @notice Thrown when an the ownership is accepted before the nomination delay + */ + error OwnershipAcceptanceTooEarly(); + + function acceptOwnership() public override { + super.acceptOwnership(); + + Guardian.Data storage store = Guardian.load(); + + if (block.timestamp.to64() - store.ownershipRequestedAt < Guardian.RESCUE_DELAY) { + revert OwnershipAcceptanceTooEarly(); + } + + store.ownershipRequestedAt = 0; + } + + function nominateNewOwner(address newNominatedOwner) public override { + Guardian.onlyGuardian(); + + OwnableStorage.Data storage ownableStore = OwnableStorage.load(); + Guardian.Data storage guardianStore = Guardian.load(); + + if (newNominatedOwner == address(0)) { + revert AddressError.ZeroAddress(); + } + + if ( + newNominatedOwner == ownableStore.nominatedOwner || + newNominatedOwner == ownableStore.owner + ) { + revert ChangeError.NoChange(); + } + + guardianStore.ownershipRequestedAt = block.timestamp.to64(); + ownableStore.nominatedOwner = newNominatedOwner; + + emit OwnerNominated(newNominatedOwner); + } + + function renounceNomination() external override { + super.renounceNomination(); + + Guardian.Data storage store = Guardian.load(); + store.ownershipRequestedAt = 0; + } } diff --git a/protocol/governance/contracts/storage/Guardian.sol b/protocol/governance/contracts/storage/Guardian.sol new file mode 100644 index 0000000000..a2909e4c43 --- /dev/null +++ b/protocol/governance/contracts/storage/Guardian.sol @@ -0,0 +1,34 @@ +//SPDX-License-Identifier: MIT +pragma solidity >=0.8.11 <0.9.0; + +import {AccessError} from "@synthetixio/core-contracts/contracts/errors/AccessError.sol"; + +library Guardian { + bytes32 private constant _STORAGE_SLOT = + keccak256(abi.encode("io.synthetix.governance.Guardian")); + + uint64 public constant RESCUE_DELAY = 7 days; + + struct Data { + address guardian; + address nominatedGuardian; + uint64 ownershipRequestedAt; + } + + function load() internal pure returns (Data storage store) { + bytes32 s = _STORAGE_SLOT; + assembly { + store.slot := s + } + } + + function onlyGuardian() internal view { + if (msg.sender != getGuardian()) { + revert AccessError.Unauthorized(msg.sender); + } + } + + function getGuardian() internal view returns (address) { + return Guardian.load().guardian; + } +}