From 95865c1bf6c4e4e8b2943ed48d907c03599cb27e Mon Sep 17 00:00:00 2001 From: koloz Date: Tue, 29 Nov 2022 11:25:13 -0500 Subject: [PATCH] broke out deadman trigger to an extension mend --- README.md | 20 ++- .../deadman-trigger/DeadmanGuardedERC1155.sol | 82 ++++++++++ .../deadman-trigger/DeadmanGuardedERC721.sol | 76 +++++++++ src/royalty-guard/IRoyaltyGuard.sol | 20 --- src/royalty-guard/RoyaltyGuard.sol | 51 ++---- .../IRoyaltyGuardDeadmanTrigger.sol | 41 +++++ .../extensions/RoyaltyGuardDeadmanTrigger.sol | 63 ++++++++ src/tests/RoyaltyGuard.t.sol | 36 ----- src/tests/RoyaltyGuardDeadmanTrigger.t.sol | 151 ++++++++++++++++++ 9 files changed, 438 insertions(+), 102 deletions(-) create mode 100644 src/example/deadman-trigger/DeadmanGuardedERC1155.sol create mode 100644 src/example/deadman-trigger/DeadmanGuardedERC721.sol create mode 100644 src/royalty-guard/extensions/IRoyaltyGuardDeadmanTrigger.sol create mode 100644 src/royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol create mode 100644 src/tests/RoyaltyGuardDeadmanTrigger.t.sol diff --git a/README.md b/README.md index a2cad7a..9176aed 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,12 @@ A general mixin for any owned/permissioned nft contract that allows for fine tun ```ml royalty-guard ├─ RoyaltyGuard — "Admin controlled ALLOW/DENY list primitives" +├─ extensions +| ├─ RoyaltyGuardDeadmanTrigger - "RoyaltyGuard with a renewable deadman switch to turn list off after predefined period of time" examples +├─ deadman-trigger +| ├─ DeadmanGuardedERC721 - "GuardedERC721 with the deadman trigger extension" +| ├─ DeadmanGuardedERC1155 - "GuardedERC1155 with the deadman trigger extension" ├─ GuardedERC721 — "Solmate based ERC721 with Owner restrictions and RoyaltyGuard" ├─ GuardedERC1155 — "Solmate based ERC1155 with Owner restrictionsand RoyaltyGuard" ``` @@ -23,12 +28,18 @@ We **do not give any warranties** and **will not be liable for any loss** incurr `RoyaltyGuard` is an abstract contract that is meant to be inherrited to integrate with a contract. The main features of the contract are: 1. Configurable list type with options `OFF`, `ALLOW`, and `DENY`. 2. Flexible admin permissioning of Guard (note examples use `Owner` based permissioning but can leave it to anyone, `ROLE` based, etc.) -3. Deadman Trigger Support - - If the owner/permissioning group hasn't renewed the trigger, anyone can come in and activate the deadman trigger turning the list type to `OFF` - - Even after deadman trigger has been activated, the owner/permissioning group can renew and change the list type to `ALLOW` or `DENY`. The two different active list types are `ALLOW` and `DENY`. In the `ALLOW` model, functions marked with the `checkList` modifier will `revert` with `Unauthorized` unless the supplied address is on the list. The `DENY` list takes the opposite approach and will `revert` if and only if the supplied address is on the list. +### Extensions + +#### Deadman Trigger + If the owner/permissioning group hasn't renewed the trigger, anyone can come in and activate the deadman trigger turning the list type to `OFF`. Even after deadman trigger has been activated, the owner/permissioning group can renew and change the list type to `ALLOW` or `DENY`. + + The two deadman trigger functions are `setDeadmanListTriggerRenewalDuration(uint256 _numYears)`, another admin guarded function, that renews the deadman switch for `_numYears` years and `activateDeadmanListTrigger()`, a public function, used to turn the list type to `OFF` and is only callable when the current `block.timestamp` is on or after the returned value from `getDeadmanTriggerAvailableDatetime()`. + + Source code at [RoyaltyGuardDeadmanTrigger](src/royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol) with [examples](src/example/deadman-trigger/). + ## How to Integrate While there are other ways to integrate with `RoyaltyGuard`, these examples use the least amount of up front code relying on post contract deployment interactions to finish setup. @@ -42,14 +53,13 @@ Example setups can be found in [GuardedERC721](src/example/GuardedERC721.sol) an For more advance setups, a set of internal functions is supplied that can be used for the purpose of setup within a contracts constructor. See [RoyaltyGuard](src/royalty-guard/RoyaltyGuard.sol). ## Usage + Guarded functions are those that are marked with the modifier `checkList(address _addr)` that checks the list type and changes based on the typing. The list type can be updated via `toggleListType(ListType _newListType)` which relies on the implemented function `hasAdminPermission(address _addr) returns (bool)`. The examples leverage use of an `Owner` to guard access. Valid inputs for `ListType` are `0` for OFF, `1` for ALLOW, and `2` for DENY. Adding, removing, and clearing a list also rely on `hasAdminPermission(address _addr) returns (bool)`. The relevant functions here are `batchAddAddressToRoyaltyList(ListType _listType, address[] _addrs)`, `batchRemoveAddressToRoyaltyList(ListType _listType, address[] _addrs)`, and `clearList(ListType _listType)`. -The two deadman trigger functions are `setDeadmanListTriggerRenewalDuration(uint256 _numYears)`, another admin guarded function, that renews the deadman switch for `_numYears` years and `activateDeadmanListTrigger()`, a public function, used to turn the list type to `OFF` and is only callable when the current `block.timestamp` is on or after the returned value from `getDeadmanTriggerAvailableDatetime()`. - ## Installation To install with [**Foundry**](https://github.com/gakonst/foundry): diff --git a/src/example/deadman-trigger/DeadmanGuardedERC1155.sol b/src/example/deadman-trigger/DeadmanGuardedERC1155.sol new file mode 100644 index 0000000..d102f19 --- /dev/null +++ b/src/example/deadman-trigger/DeadmanGuardedERC1155.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {ERC1155} from "solmate/tokens/ERC1155.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {RoyaltyGuardDeadmanTrigger, RoyaltyGuard, IRoyaltyGuard} from "../../royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol"; + +contract DeadmanGuardedERC1155 is ERC1155, Owned, RoyaltyGuardDeadmanTrigger { + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STORAGE + //////////////////////////////////////////////////////////////////////////*/ + string private baseURI; + string private name_; + string private symbol_; + uint256 private tokenIdCounter; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + constructor(string memory _name, string memory _symbol, string memory _baseURI, address _newOwner) + Owned(_newOwner) + { + baseURI = _baseURI; + name_ = _name; + symbol_ = _symbol; + } + + /*////////////////////////////////////////////////////////////////////////// + ERC1155 LOGIC + //////////////////////////////////////////////////////////////////////////*/ + function uri(uint256 _id) public view virtual override returns (string memory) { + return string(abi.encodePacked(baseURI, _id)); + } + + /// @notice Returns the name of this 1155 contract. + /// @return name of contract + function name() external view returns (string memory) { + return name_; + } + + /// @notice Returns the symbol of this 1155 contract. + /// @return symbol of contract + function symbol() external view returns (string memory) { + return symbol_; + } + + /// @notice Create a new token sending the full {_amount} to {_to}. + /// @dev Must be contract owner to mint new token. + function mint(address _to, uint256 _amount) external onlyOwner{ + _mint(_to, tokenIdCounter++, _amount, ""); + } + + /// @notice Destroy {_amount} of token with id {_id}. + /// @dev Must have a balance >= {_amount} of {_tokenId}. + function burn(address _from, uint256 _id, uint256 _amount) external { + if (balanceOf[_from][_id] < _amount) revert("INSUFFICIENT BALANCE"); + _burn(_from, _id, _amount); + } + + /*////////////////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 _interfaceId) public view virtual override (ERC1155, RoyaltyGuard) returns (bool) { + return RoyaltyGuard.supportsInterface(_interfaceId) || ERC1155.supportsInterface(_interfaceId); + } + + /*////////////////////////////////////////////////////////////////////////// + RoyaltyGuard LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc RoyaltyGuard + function hasAdminPermission(address _addr) public view virtual override (IRoyaltyGuard, RoyaltyGuard) returns (bool) { + return _addr == owner; + } + + /// @dev Guards {setApprovalForAll} based on the type of list and depending if {_operator} is on the list. + function setApprovalForAll(address _operator, bool _approved) public virtual override checkList(_operator) { + super.setApprovalForAll(_operator, _approved); + } +} diff --git a/src/example/deadman-trigger/DeadmanGuardedERC721.sol b/src/example/deadman-trigger/DeadmanGuardedERC721.sol new file mode 100644 index 0000000..069d637 --- /dev/null +++ b/src/example/deadman-trigger/DeadmanGuardedERC721.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {ERC721} from "solmate/tokens/ERC721.sol"; +import {Owned} from "solmate/auth/Owned.sol"; +import {RoyaltyGuardDeadmanTrigger, RoyaltyGuard, IRoyaltyGuard} from "../../royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol"; + +contract DeadmanGuardedERC721 is ERC721, Owned, RoyaltyGuardDeadmanTrigger { + + /*////////////////////////////////////////////////////////////////////////// + PRIVATE STORAGE + //////////////////////////////////////////////////////////////////////////*/ + string private baseURI; + uint256 private tokenCounter; + + /*////////////////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////////////////*/ + constructor(string memory _name, string memory _symbol, string memory _baseURI, address _newOwner) + ERC721(_name, _symbol) + Owned(_newOwner) + { + baseURI = _baseURI; + } + + /*////////////////////////////////////////////////////////////////////////// + ERC721 LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Retrieve the tokenURI for a token with the supplied _id + /// @dev Wont throw or revert given a nonexistent tokenId + /// @return string token uri + function tokenURI(uint256 _id) public view virtual override returns (string memory) { + return string(abi.encodePacked(baseURI, _id)); + } + + /// @notice Create a new token sending directly to {_to}. + /// @dev Must be contract owner to mint new token. + function mint(address _to) external onlyOwner { + _mint(_to, tokenCounter++); + } + + /// @notice Destroy token with id {_id}. + /// @dev Must be the token owner to call. + function burn(uint256 _id) external { + if (_ownerOf[_id] != msg.sender) revert("NOT_OWNER"); + _burn(_id); + } + + /*////////////////////////////////////////////////////////////////////////// + ERC165 LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + function supportsInterface(bytes4 _interfaceId) public view virtual override (ERC721, RoyaltyGuard) returns (bool) { + return RoyaltyGuard.supportsInterface(_interfaceId) || ERC721.supportsInterface(_interfaceId); + } + + /*////////////////////////////////////////////////////////////////////////// + RoyaltyGuard LOGIC + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc RoyaltyGuard + function hasAdminPermission(address _addr) public view virtual override (IRoyaltyGuard, RoyaltyGuard) returns (bool) { + return _addr == owner; + } + + /// @dev Guards {approve} based on the type of list and depending if {_spender} is on the list. + function approve(address _spender, uint256 _id) public virtual override checkList(_spender) { + super.approve(_spender, _id); + } + + /// @dev Guards {setApprovalForAll} based on the type of list and depending if {_operator} is on the list. + function setApprovalForAll(address _operator, bool _approved) public virtual override checkList(_operator) { + super.setApprovalForAll(_operator, _approved); + } +} diff --git a/src/royalty-guard/IRoyaltyGuard.sol b/src/royalty-guard/IRoyaltyGuard.sol index 76dc7cb..37c4a42 100644 --- a/src/royalty-guard/IRoyaltyGuard.sol +++ b/src/royalty-guard/IRoyaltyGuard.sol @@ -30,15 +30,9 @@ interface IRoyaltyGuard { /// @notice Emitted when an address is removed from a list. event AddressRemovedList(address indexed _updater, address indexed _removedAddr, ListType indexed _ListType); - /// @notice Emitted when deadman trigger datetime has been updated. - event DeadmanTriggerDatetimeUpdated(address indexed _updater, uint256 _oldDatetime, uint256 _newDatetime); - /// @notice Emitted when a list is cleared. event ListCleared(address indexed _updater, ListType _listType); - /// @notice Emitted when the deadman switch is activated. - event DeadmanTriggerActivated(address indexed _activator); - /*////////////////////////////////////////////////////////////////////////// Custom Errors //////////////////////////////////////////////////////////////////////////*/ @@ -49,9 +43,6 @@ interface IRoyaltyGuard { /// @notice Emitted when trying to add an address to a list with type OFF. error CantAddToOFFList(); - /// @notice Emitted when the deadman trigger datetime threshold hasnt passed but tries to get called. - error DeadmanTriggerStillActive(); - /// @notice Emitted when an admin only function tries to be called by a non-admin. error MustBeAdmin(); @@ -73,17 +64,10 @@ interface IRoyaltyGuard { /// @param _addrs being removed from the designated list function batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType _listType, address[] calldata _addrs) external; - /// @notice Sets the deadman list trigger for the specified number of years from current block timestamp - /// @param _numYears to renew the trigger for. - function setDeadmanListTriggerRenewalDuration(uint256 _numYears) external; - /// @notice Clears an entire list. /// @param _listType of list being cleared. function clearList(IRoyaltyGuard.ListType _listType) external; - /// @notice Triggers the deadman switch for the list - function activateDeadmanListTrigger() external; - /*////////////////////////////////////////////////////////////////////////// External Read Functions //////////////////////////////////////////////////////////////////////////*/ @@ -110,8 +94,4 @@ interface IRoyaltyGuard { /// @notice Returns the ListType currently being used; /// @return ListType of the list. Values are: 0 (OFF), 1 (ALLOW), 2 (DENY) function getListType() external view returns (ListType); - - /// @notice The datetime threshold after which the deadman trigger can be called by anyone. - /// @return uint256 denoting unix epoch time after which the deadman trigger can be activated. - function getDeadmanTriggerAvailableDatetime() external view returns (uint256); } diff --git a/src/royalty-guard/RoyaltyGuard.sol b/src/royalty-guard/RoyaltyGuard.sol index eff6b6c..8f66808 100644 --- a/src/royalty-guard/RoyaltyGuard.sol +++ b/src/royalty-guard/RoyaltyGuard.sol @@ -19,7 +19,6 @@ abstract contract RoyaltyGuard is IRoyaltyGuard, ERC165 { mapping(IRoyaltyGuard.ListType => EnumerableSet.AddressSet) private list; IRoyaltyGuard.ListType private listType; - uint256 public deadmanListTriggerAfterDatetime; /*////////////////////////////////////////////////////////////////////////// Modifiers @@ -50,88 +49,65 @@ abstract contract RoyaltyGuard is IRoyaltyGuard, ERC165 { /// @dev Only the contract owner can call this function. /// @inheritdoc IRoyaltyGuard - function toggleListType(IRoyaltyGuard.ListType _newListType) external onlyAdmin { + function toggleListType(IRoyaltyGuard.ListType _newListType) external virtual onlyAdmin { _setListType(_newListType); } /// @dev Only the contract owner can call this function. /// @dev Cannot add to the OFF list /// @inheritdoc IRoyaltyGuard - function batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType _listType, address[] calldata _addrs) external onlyAdmin { + function batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType _listType, address[] calldata _addrs) external virtual onlyAdmin { if (_listType == IRoyaltyGuard.ListType.OFF) revert IRoyaltyGuard.CantAddToOFFList(); _batchUpdateList(_listType, _addrs, true); } /// @dev Only the contract owner can call this function. /// @inheritdoc IRoyaltyGuard - function batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType _listType, address[] calldata _addrs) external onlyAdmin { + function batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType _listType, address[] calldata _addrs) external virtual onlyAdmin { _batchUpdateList(_listType, _addrs, false); } /// @dev Only the contract owner can call this function. /// @inheritdoc IRoyaltyGuard - function setDeadmanListTriggerRenewalDuration(uint256 _numYears) external onlyAdmin { - _setDeadmanTriggerRenewalInYears(_numYears); - } - - /// @dev Only the contract owner can call this function. - /// @inheritdoc IRoyaltyGuard - function clearList(IRoyaltyGuard.ListType _listType) external onlyAdmin { + function clearList(IRoyaltyGuard.ListType _listType) external virtual onlyAdmin { delete list[_listType]; emit ListCleared(msg.sender, _listType); } - /*////////////////////////////////////////////////////////////////////////// - Public Write Functions - //////////////////////////////////////////////////////////////////////////*/ - - /// @dev Can only be called if deadmanListTriggerAfterDatetime is in the past. - /// @inheritdoc IRoyaltyGuard - function activateDeadmanListTrigger() external { - if (deadmanListTriggerAfterDatetime > block.timestamp) revert IRoyaltyGuard.DeadmanTriggerStillActive(); - listType = IRoyaltyGuard.ListType.OFF; - emit DeadmanTriggerActivated(msg.sender); - } - /*////////////////////////////////////////////////////////////////////////// Public Read Functions //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc IRoyaltyGuard - function getList(IRoyaltyGuard.ListType _listType) external view returns (address[] memory) { + function getList(IRoyaltyGuard.ListType _listType) external virtual view returns (address[] memory) { return list[_listType].values(); } /// @inheritdoc IRoyaltyGuard - function getInUseList() external view returns (address[] memory) { + function getInUseList() external virtual view returns (address[] memory) { return list[listType].values(); } /// @inheritdoc IRoyaltyGuard - function isOperatorInList(address _operator) external view returns (bool) { + function isOperatorInList(address _operator) external virtual view returns (bool) { return list[listType].contains(_operator); } /// @inheritdoc IRoyaltyGuard - function getListType() external view returns (IRoyaltyGuard.ListType) { + function getListType() external virtual view returns (IRoyaltyGuard.ListType) { return listType; } - /// @inheritdoc IRoyaltyGuard - function getDeadmanTriggerAvailableDatetime() external view returns (uint256) { - return deadmanListTriggerAfterDatetime; - } - /// @dev used in the {onlyAdmin} modifier /// @inheritdoc IRoyaltyGuard - function hasAdminPermission(address _addr) public view virtual returns (bool); + function hasAdminPermission(address _addr) public virtual view returns (bool); /*////////////////////////////////////////////////////////////////////////// ERC165 Overrides //////////////////////////////////////////////////////////////////////////*/ /// @inheritdoc ERC165 - function supportsInterface(bytes4 _interfaceId) public view virtual override returns (bool) { + function supportsInterface(bytes4 _interfaceId) public virtual view override returns (bool) { return _interfaceId == type(IRoyaltyGuard).interfaceId || super.supportsInterface(_interfaceId); } @@ -159,11 +135,4 @@ abstract contract RoyaltyGuard is IRoyaltyGuard, ERC165 { } } } - - /// @dev Internal method to set deadman trigger datetime. Main usage is constructor. - function _setDeadmanTriggerRenewalInYears(uint256 _numYears) internal { - uint256 newDatetime = block.timestamp + _numYears * 365 days; - emit DeadmanTriggerDatetimeUpdated(msg.sender, deadmanListTriggerAfterDatetime, newDatetime); - deadmanListTriggerAfterDatetime = newDatetime; - } } diff --git a/src/royalty-guard/extensions/IRoyaltyGuardDeadmanTrigger.sol b/src/royalty-guard/extensions/IRoyaltyGuardDeadmanTrigger.sol new file mode 100644 index 0000000..69c02cd --- /dev/null +++ b/src/royalty-guard/extensions/IRoyaltyGuardDeadmanTrigger.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IRoyaltyGuard} from "../IRoyaltyGuard.sol"; + +/// @title IRoyaltyGuard +/// @author highland, koloz, van arman +/// @notice Interface for a deadman trigger extension to IRoyaltyGuard +interface IRoyaltyGuardDeadmanTrigger is IRoyaltyGuard { + /*////////////////////////////////////////////////////////////////////////// + Events + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when deadman trigger datetime has been updated. + event DeadmanTriggerDatetimeUpdated(address indexed _updater, uint256 _oldDatetime, uint256 _newDatetime); + + /// @notice Emitted when the deadman switch is activated. + event DeadmanTriggerActivated(address indexed _activator); + + /*////////////////////////////////////////////////////////////////////////// + Custom Errors + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when the deadman trigger datetime threshold hasnt passed but tries to get called. + error DeadmanTriggerStillActive(); + + /*////////////////////////////////////////////////////////////////////////// + External Write Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Sets the deadman list trigger for the specified number of years from current block timestamp + /// @param _numYears to renew the trigger for. + function setDeadmanListTriggerRenewalDuration(uint256 _numYears) external; + + /// @notice Triggers the deadman switch for the list + function activateDeadmanListTrigger() external; + + /// @notice The datetime threshold after which the deadman trigger can be called by anyone. + /// @return uint256 denoting unix epoch time after which the deadman trigger can be activated. + function getDeadmanTriggerAvailableDatetime() external view returns (uint256); +} \ No newline at end of file diff --git a/src/royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol b/src/royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol new file mode 100644 index 0000000..88f01b4 --- /dev/null +++ b/src/royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {IRoyaltyGuardDeadmanTrigger} from "../extensions/IRoyaltyGuardDeadmanTrigger.sol"; +import {IRoyaltyGuard, RoyaltyGuard} from "../RoyaltyGuard.sol"; + +import {EnumerableSet} from "openzeppelin-contracts/utils/structs/EnumerableSet.sol"; +import {ERC165} from "openzeppelin-contracts/utils/introspection/ERC165.sol"; + +/// @title RoyaltyGuard +/// @author highland, koloz, van arman +/// @notice An abstract contract with the necessary functions, structures, modifiers to ensure royalties are paid. +/// @dev Inherriting this contract requires implementing {hasAdminPermission} and connecting the desired functions to the {checkList} modifier. +abstract contract RoyaltyGuardDeadmanTrigger is IRoyaltyGuardDeadmanTrigger, RoyaltyGuard { + + /*////////////////////////////////////////////////////////////////////////// + Private Contract Storage + //////////////////////////////////////////////////////////////////////////*/ + + uint256 private deadmanListTriggerAfterDatetime; + + /*////////////////////////////////////////////////////////////////////////// + Admin Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Only the contract owner can call this function. + /// @inheritdoc IRoyaltyGuardDeadmanTrigger + function setDeadmanListTriggerRenewalDuration(uint256 _numYears) external virtual onlyAdmin { + _setDeadmanTriggerRenewalInYears(_numYears); + } + + /*////////////////////////////////////////////////////////////////////////// + Public Write Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Can only be called if deadmanListTriggerAfterDatetime is in the past. + /// @inheritdoc IRoyaltyGuardDeadmanTrigger + function activateDeadmanListTrigger() external virtual { + if (deadmanListTriggerAfterDatetime > block.timestamp) revert IRoyaltyGuardDeadmanTrigger.DeadmanTriggerStillActive(); + _setListType(IRoyaltyGuard.ListType.OFF); + emit DeadmanTriggerActivated(msg.sender); + } + + /*////////////////////////////////////////////////////////////////////////// + Public Read Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IRoyaltyGuardDeadmanTrigger + function getDeadmanTriggerAvailableDatetime() external virtual view returns (uint256) { + return deadmanListTriggerAfterDatetime; + } + + /*////////////////////////////////////////////////////////////////////////// + Internal Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev Internal method to set deadman trigger datetime. Main usage is constructor. + function _setDeadmanTriggerRenewalInYears(uint256 _numYears) internal { + uint256 newDatetime = block.timestamp + _numYears * 365 days; + emit DeadmanTriggerDatetimeUpdated(msg.sender, deadmanListTriggerAfterDatetime, newDatetime); + deadmanListTriggerAfterDatetime = newDatetime; + } +} diff --git a/src/tests/RoyaltyGuard.t.sol b/src/tests/RoyaltyGuard.t.sol index 2437456..939a987 100644 --- a/src/tests/RoyaltyGuard.t.sol +++ b/src/tests/RoyaltyGuard.t.sol @@ -11,7 +11,6 @@ contract RoyaltyGuardOwner is RoyaltyGuard { owner = _owner; _setListType(_listType); _batchUpdateList(_listType, _addrs, true); - _setDeadmanTriggerRenewalInYears(1); } function hasAdminPermission(address _addr) public view override returns (bool) { @@ -46,11 +45,6 @@ contract RoyaltyGuardTest is Test { assert(listType == IRoyaltyGuard.ListType.ALLOW); } - function testDeadmanTriggerDatetime() public view { - uint256 deadmanTriggerThreshold = guard.getDeadmanTriggerAvailableDatetime(); - assert(deadmanTriggerThreshold == deployDatetime + 365 days); - } - function testListValues() public view { address[] memory addrs = guard.getInUseList(); assert(addrs.length == 1); @@ -95,33 +89,6 @@ contract RoyaltyGuardTest is Test { guard.testCheckList(_addr); } - function testDeadmanTrigger() public { - vm.expectRevert(IRoyaltyGuard.DeadmanTriggerStillActive.selector); - guard.activateDeadmanListTrigger(); - - vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); - guard.testCheckList(charlie); - - vm.warp(deployDatetime + 365 days); - guard.activateDeadmanListTrigger(); - - IRoyaltyGuard.ListType listType = guard.getListType(); - assert(listType == IRoyaltyGuard.ListType.OFF); - - guard.testCheckList(charlie); - - vm.startPrank(alice); - guard.setDeadmanListTriggerRenewalDuration(1); - guard.toggleListType(IRoyaltyGuard.ListType.ALLOW); - vm.stopPrank(); - - vm.expectRevert(IRoyaltyGuard.DeadmanTriggerStillActive.selector); - guard.activateDeadmanListTrigger(); - - vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); - guard.testCheckList(charlie); - } - function testAdminFunctions() public { address[] memory addrs = new address[](1); addrs[0] = charlie; @@ -130,7 +97,6 @@ contract RoyaltyGuardTest is Test { guard.toggleListType(IRoyaltyGuard.ListType.DENY); guard.batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); guard.batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); - guard.setDeadmanListTriggerRenewalDuration(1); guard.clearList(IRoyaltyGuard.ListType.DENY); vm.stopPrank(); @@ -142,8 +108,6 @@ contract RoyaltyGuardTest is Test { vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); guard.batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); - guard.setDeadmanListTriggerRenewalDuration(1); - vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); guard.clearList(IRoyaltyGuard.ListType.DENY); vm.stopPrank(); } diff --git a/src/tests/RoyaltyGuardDeadmanTrigger.t.sol b/src/tests/RoyaltyGuardDeadmanTrigger.t.sol new file mode 100644 index 0000000..0813685 --- /dev/null +++ b/src/tests/RoyaltyGuardDeadmanTrigger.t.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity 0.8.15; + +import "forge-std/Test.sol"; +import {IRoyaltyGuard, RoyaltyGuard} from "../royalty-guard/RoyaltyGuard.sol"; +import {IRoyaltyGuardDeadmanTrigger, RoyaltyGuardDeadmanTrigger} from "../royalty-guard/extensions/RoyaltyGuardDeadmanTrigger.sol"; + +contract RoyaltyGuardOwner is RoyaltyGuardDeadmanTrigger { + address public owner; + + constructor(address _owner, IRoyaltyGuard.ListType _listType, address[] memory _addrs) { + owner = _owner; + _setListType(_listType); + _batchUpdateList(_listType, _addrs, true); + _setDeadmanTriggerRenewalInYears(1); + } + + function hasAdminPermission(address _addr) public view override (IRoyaltyGuard, RoyaltyGuard) returns (bool) { + return _addr == owner; + } + + function testCheckList(address _addr) external checkList(_addr) {} +} + +contract RoyaltyGuardDeadmanTriggerTest is Test { + RoyaltyGuardOwner guard; + address alice; + address bob; + address charlie; + uint256 deployDatetime; + + function setUp() public { + alice = address(0x1337); + bob = address(0xBEEF); + charlie = address(0xCAFE); + + address[] memory allowList = new address[](1); + allowList[0] = bob; + + guard = new RoyaltyGuardOwner(alice, IRoyaltyGuard.ListType.ALLOW, allowList); + + deployDatetime = block.timestamp; + } + + function testGetListType() public view { + IRoyaltyGuard.ListType listType = guard.getListType(); + assert(listType == IRoyaltyGuard.ListType.ALLOW); + } + + function testDeadmanTriggerDatetime() public view { + uint256 deadmanTriggerThreshold = guard.getDeadmanTriggerAvailableDatetime(); + assert(deadmanTriggerThreshold == deployDatetime + 365 days); + } + + function testListValues() public view { + address[] memory addrs = guard.getInUseList(); + assert(addrs.length == 1); + assert(addrs[0] == bob); + } + + function testCheckList_ALLOW(address _addr) public { + if (_addr != bob) { + vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); + } + + guard.testCheckList(_addr); + + address[] memory addrs = new address[](1); + addrs[0] = _addr; + + vm.prank(alice); + guard.batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType.ALLOW, addrs); + + guard.testCheckList(_addr); + } + + function testCheckList_DENY(address _addr) public { + address[] memory denyList = new address[](1); + denyList[0] = bob; + + guard = new RoyaltyGuardOwner(alice, IRoyaltyGuard.ListType.DENY, denyList); + + if (_addr == bob) { + vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); + } + + guard.testCheckList(_addr); + + address[] memory addrs = new address[](1); + addrs[0] = _addr; + + vm.prank(alice); + guard.batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); + + vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); + guard.testCheckList(_addr); + } + + function testDeadmanTrigger() public { + vm.expectRevert(IRoyaltyGuardDeadmanTrigger.DeadmanTriggerStillActive.selector); + guard.activateDeadmanListTrigger(); + + vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); + guard.testCheckList(charlie); + + vm.warp(deployDatetime + 365 days); + guard.activateDeadmanListTrigger(); + + IRoyaltyGuard.ListType listType = guard.getListType(); + assert(listType == IRoyaltyGuard.ListType.OFF); + + guard.testCheckList(charlie); + + vm.startPrank(alice); + guard.setDeadmanListTriggerRenewalDuration(1); + guard.toggleListType(IRoyaltyGuard.ListType.ALLOW); + vm.stopPrank(); + + vm.expectRevert(IRoyaltyGuardDeadmanTrigger.DeadmanTriggerStillActive.selector); + guard.activateDeadmanListTrigger(); + + vm.expectRevert(IRoyaltyGuard.Unauthorized.selector); + guard.testCheckList(charlie); + } + + function testAdminFunctions() public { + address[] memory addrs = new address[](1); + addrs[0] = charlie; + + vm.startPrank(alice); + guard.toggleListType(IRoyaltyGuard.ListType.DENY); + guard.batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); + guard.batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); + guard.setDeadmanListTriggerRenewalDuration(1); + guard.clearList(IRoyaltyGuard.ListType.DENY); + vm.stopPrank(); + + vm.startPrank(bob); + vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); + guard.toggleListType(IRoyaltyGuard.ListType.DENY); + vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); + guard.batchAddAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); + vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); + guard.batchRemoveAddressToRoyaltyList(IRoyaltyGuard.ListType.DENY, addrs); + vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); + guard.setDeadmanListTriggerRenewalDuration(1); + vm.expectRevert(IRoyaltyGuard.MustBeAdmin.selector); + guard.clearList(IRoyaltyGuard.ListType.DENY); + vm.stopPrank(); + } +}