From 8ae044e53401f8fd576245198dc895a0931e3117 Mon Sep 17 00:00:00 2001 From: Schlagonia Date: Fri, 26 Jan 2024 20:10:40 -0700 Subject: [PATCH] build: granular roles --- src/BaseStrategy.sol | 15 +++++-- src/TokenizedStrategy.sol | 57 +++++++++++++++++++++++++-- src/interfaces/IEvents.sol | 6 +++ src/interfaces/ITokenizedStrategy.sol | 11 ++++++ src/test/AccessControl.t.sol | 35 ++++++++++++++++ 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/BaseStrategy.sol b/src/BaseStrategy.sol index ef438ba..21f7f29 100644 --- a/src/BaseStrategy.sol +++ b/src/BaseStrategy.sol @@ -52,7 +52,11 @@ abstract contract BaseStrategy { * @dev Use to assure that the call is coming from the strategies management. */ modifier onlyManagement() { - require(TokenizedStrategy.isManagement(msg.sender), "!management"); + require( + TokenizedStrategy.isManagement(msg.sender) || + TokenizedStrategy.isAllowed(msg.sig, msg.sender), + "!management" + ); _; } @@ -61,7 +65,11 @@ abstract contract BaseStrategy { * management or the keeper. */ modifier onlyKeepers() { - require(TokenizedStrategy.isKeeperOrManagement(msg.sender), "!keeper"); + require( + TokenizedStrategy.isKeeperOrManagement(msg.sender) || + TokenizedStrategy.isAllowed(msg.sig, msg.sender), + "!keeper" + ); _; } @@ -71,7 +79,8 @@ abstract contract BaseStrategy { */ modifier onlyEmergencyAuthorized() { require( - TokenizedStrategy.isEmergencyAuthorized(msg.sender), + TokenizedStrategy.isEmergencyAuthorized(msg.sender) || + TokenizedStrategy.isAllowed(msg.sig, msg.sender), "!emergency authorized" ); _; diff --git a/src/TokenizedStrategy.sol b/src/TokenizedStrategy.sol index 2430b77..0d21ae2 100644 --- a/src/TokenizedStrategy.sol +++ b/src/TokenizedStrategy.sol @@ -117,6 +117,15 @@ contract TokenizedStrategy { */ event UpdateProfitMaxUnlockTime(uint256 newProfitMaxUnlockTime); + /** + * @notice Emitted when the a `sender`s status for a certain `selector` is updated. + */ + event UpdateAllowed( + bytes4 indexed selector, + address indexed sender, + bool indexed allowed + ); + /** * @notice Emitted when a strategy is shutdown. */ @@ -242,6 +251,9 @@ contract TokenizedStrategy { address keeper; // Address given permission to call {report} and {tend}. address pendingManagement; // Address that is pending to take over `management`. address emergencyAdmin; // Address to act in emergencies as well as `management`. + mapping(bytes4 => mapping(address => bool)) allowed; // Mapping from a function selector to if an address can call it. + + // Strategy status checks. bool entered; // Bool to prevent reentrancy. bool shutdown; // Bool that can be used to stop deposits into the strategy. } @@ -254,7 +266,10 @@ contract TokenizedStrategy { * @dev Require that the call is coming from the strategies management. */ modifier onlyManagement() { - require(isManagement(msg.sender), "!management"); + require( + isManagement(msg.sender) || isAllowed(msg.sig, msg.sender), + "!management" + ); _; } @@ -263,7 +278,10 @@ contract TokenizedStrategy { * management or the keeper. */ modifier onlyKeepers() { - require(isKeeperOrManagement(msg.sender), "!keeper"); + require( + isKeeperOrManagement(msg.sender) || isAllowed(msg.sig, msg.sender), + "!keeper" + ); _; } @@ -272,7 +290,10 @@ contract TokenizedStrategy { * management or the emergency admin. */ modifier onlyEmergencyAuthorized() { - require(isEmergencyAuthorized(msg.sender), "!emergency authorized"); + require( + isEmergencyAuthorized(msg.sender) || isAllowed(msg.sig, msg.sender), + "!emergency authorized" + ); _; } @@ -1481,6 +1502,26 @@ contract TokenizedStrategy { return _convertToAssets(S, 10 ** S.decimals); } + /** + * @notice To check if a sender was given the ability to call + * as specific function based on its selector. + * @dev Is left public so that it can be used by the Strategy. + * + * When the Strategy calls this the msg.sender would be the + * address of the strategy so we need to specify the sender. + * + * Will return `true` if the check passed. + * + * @param _selector The functions selector. + * @param _sender The original msg.sender. + */ + function isAllowed( + bytes4 _selector, + address _sender + ) public view returns (bool) { + return _strategyStorage().allowed[_selector][_sender]; + } + /** * @notice To check if the strategy has been shutdown. * @return . Whether or not the strategy is shutdown. @@ -1619,6 +1660,16 @@ contract TokenizedStrategy { emit UpdateProfitMaxUnlockTime(_profitMaxUnlockTime); } + function setAllowed( + bytes4 _selector, + address _sender, + bool _allowed + ) external onlyManagement { + _strategyStorage().allowed[_selector][_sender] = _allowed; + + emit UpdateAllowed(_selector, _sender, _allowed); + } + /*////////////////////////////////////////////////////////////// ERC20 METHODS //////////////////////////////////////////////////////////////*/ diff --git a/src/interfaces/IEvents.sol b/src/interfaces/IEvents.sol index 90fc32a..f1db3b4 100644 --- a/src/interfaces/IEvents.sol +++ b/src/interfaces/IEvents.sol @@ -44,6 +44,12 @@ interface IEvents { */ event UpdateProfitMaxUnlockTime(uint256 newProfitMaxUnlockTime); + event UpdateAllowed( + bytes4 indexed selector, + address indexed sender, + bool indexed allowed + ); + /** * @notice Emitted when a strategy is shutdown. */ diff --git a/src/interfaces/ITokenizedStrategy.sol b/src/interfaces/ITokenizedStrategy.sol index c610350..f3858ff 100644 --- a/src/interfaces/ITokenizedStrategy.sol +++ b/src/interfaces/ITokenizedStrategy.sol @@ -78,6 +78,11 @@ interface ITokenizedStrategy is IERC4626, IERC20Permit { address _sender ) external view returns (bool); + function isAllowed( + bytes4 _selector, + address _sender + ) external view returns (bool); + /*////////////////////////////////////////////////////////////// KEEPERS FUNCTIONS //////////////////////////////////////////////////////////////*/ @@ -148,6 +153,12 @@ interface ITokenizedStrategy is IERC4626, IERC20Permit { function setProfitMaxUnlockTime(uint256 _profitMaxUnlockTime) external; + function setAllowed( + bytes4 _selector, + address _sender, + bool _allowed + ) external; + function shutdownStrategy() external; function emergencyWithdraw(uint256 _amount) external; diff --git a/src/test/AccessControl.t.sol b/src/test/AccessControl.t.sol index 4c1367c..e3a27a8 100644 --- a/src/test/AccessControl.t.sol +++ b/src/test/AccessControl.t.sol @@ -81,6 +81,41 @@ contract AccessControlTest is Setup { assertEq(strategy.profitMaxUnlockTime(), amount); } + function test_setAllowed(address _toAllow) public { + vm.assume(_toAllow != management && _toAllow != keeper); + + bytes4 _report = tokenizedStrategy.report.selector; + + assertTrue(!strategy.isAllowed(_report, _toAllow)); + + vm.expectRevert("!keeper"); + vm.prank(_toAllow); + strategy.report(); + + vm.expectEmit(true, true, true, true, address(strategy)); + emit UpdateAllowed(_report, _toAllow, true); + + vm.prank(management); + strategy.setAllowed(_report, _toAllow, true); + + assertTrue(strategy.isAllowed(_report, _toAllow)); + + vm.prank(_toAllow); + strategy.report(); + + vm.expectEmit(true, true, true, true, address(strategy)); + emit UpdateAllowed(_report, _toAllow, false); + + vm.prank(management); + strategy.setAllowed(_report, _toAllow, false); + + assertTrue(!strategy.isAllowed(_report, _toAllow)); + + vm.expectRevert("!keeper"); + vm.prank(_toAllow); + strategy.report(); + } + function test_shutdown() public { assertTrue(!strategy.isShutdown());