diff --git a/src/PublicAllocator.sol b/src/PublicAllocator.sol index da40eae..960136d 100644 --- a/src/PublicAllocator.sol +++ b/src/PublicAllocator.sol @@ -37,7 +37,7 @@ contract PublicAllocator is IPublicAllocatorStaticTyping { /* STORAGE */ /// @inheritdoc IPublicAllocatorBase - mapping(address => address) public owner; + mapping(address => address) public admin; /// @inheritdoc IPublicAllocatorBase mapping(address => uint256) public fee; /// @inheritdoc IPublicAllocatorBase @@ -47,9 +47,11 @@ contract PublicAllocator is IPublicAllocatorStaticTyping { /* MODIFIER */ - /// @dev Reverts if the caller is not the owner for this vault or the vault owner. - modifier onlyOwner(address vault) { - if (msg.sender != owner[vault] && msg.sender != IMetaMorpho(vault).owner()) revert ErrorsLib.NotOwner(); + /// @dev Reverts if the caller is not the admin nor the owner of this vault. + modifier onlyAdminOrVaultOwner(address vault) { + if (msg.sender != admin[vault] && msg.sender != IMetaMorpho(vault).owner()) { + revert ErrorsLib.NotAdminNorVaultOwner(); + } _; } @@ -60,24 +62,24 @@ contract PublicAllocator is IPublicAllocatorStaticTyping { MORPHO = IMorpho(morpho); } - /* OWNER ONLY */ + /* ADMIN OR VAULT OWNER ONLY */ /// @inheritdoc IPublicAllocatorBase - function setOwner(address vault, address newOwner) external onlyOwner(vault) { - if (owner[vault] == newOwner) revert ErrorsLib.AlreadySet(); - owner[vault] = newOwner; - emit EventsLib.SetOwner(msg.sender, vault, newOwner); + function setAdmin(address vault, address newAdmin) external onlyAdminOrVaultOwner(vault) { + if (admin[vault] == newAdmin) revert ErrorsLib.AlreadySet(); + admin[vault] = newAdmin; + emit EventsLib.SetAdmin(msg.sender, vault, newAdmin); } /// @inheritdoc IPublicAllocatorBase - function setFee(address vault, uint256 newFee) external onlyOwner(vault) { + function setFee(address vault, uint256 newFee) external onlyAdminOrVaultOwner(vault) { if (fee[vault] == newFee) revert ErrorsLib.AlreadySet(); fee[vault] = newFee; emit EventsLib.SetFee(msg.sender, vault, newFee); } /// @inheritdoc IPublicAllocatorBase - function setFlowCaps(address vault, FlowCapsConfig[] calldata config) external onlyOwner(vault) { + function setFlowCaps(address vault, FlowCapsConfig[] calldata config) external onlyAdminOrVaultOwner(vault) { for (uint256 i = 0; i < config.length; i++) { if (config[i].caps.maxIn > MAX_SETTABLE_FLOW_CAP || config[i].caps.maxOut > MAX_SETTABLE_FLOW_CAP) { revert ErrorsLib.MaxSettableFlowCapExceeded(); @@ -89,7 +91,7 @@ contract PublicAllocator is IPublicAllocatorStaticTyping { } /// @inheritdoc IPublicAllocatorBase - function transferFee(address vault, address payable feeRecipient) external onlyOwner(vault) { + function transferFee(address vault, address payable feeRecipient) external onlyAdminOrVaultOwner(vault) { uint256 claimed = accruedFee[vault]; accruedFee[vault] = 0; feeRecipient.transfer(claimed); diff --git a/src/interfaces/IPublicAllocator.sol b/src/interfaces/IPublicAllocator.sol index 8457d67..56db832 100644 --- a/src/interfaces/IPublicAllocator.sol +++ b/src/interfaces/IPublicAllocator.sol @@ -38,12 +38,11 @@ struct Withdrawal { /// @dev This interface is used for factorizing IPublicAllocatorStaticTyping and IPublicAllocator. /// @dev Consider using the IPublicAllocator interface instead of this one. interface IPublicAllocatorBase { - /// @notice The address of the Morpho contract. + /// @notice The Morpho contract. function MORPHO() external view returns (IMorpho); - /// @notice The address of the owner of the public allocator config for a given vault. - /// @dev The owner of the underlying vault always has the public allocator owner capabilities. - function owner(address vault) external view returns (address); + /// @notice The admin for a given vault. + function admin(address vault) external view returns (address); /// @notice The current ETH fee for a given vault. function fee(address vault) external view returns (uint256); @@ -64,8 +63,8 @@ interface IPublicAllocatorBase { external payable; - /// @notice Sets the owner for a given vault. - function setOwner(address vault, address newOwner) external; + /// @notice Sets the admin for a given vault. + function setAdmin(address vault, address newAdmin) external; /// @notice Sets the fee for a given vault. function setFee(address vault, uint256 newFee) external; diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index df939aa..42b5ecb 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -8,8 +8,8 @@ import {Id} from "../../lib/metamorpho/src/interfaces/IMetaMorpho.sol"; /// @custom:contact security@morpho.org /// @notice Library exposing error messages. library ErrorsLib { - /// @notice Thrown when the `msg.sender` is not the `owner`. - error NotOwner(); + /// @notice Thrown when the `msg.sender` is not the admin nor the owner of the vault. + error NotAdminNorVaultOwner(); /// @notice Thrown when the reallocation fee given is wrong. error IncorrectFee(); diff --git a/src/libraries/EventsLib.sol b/src/libraries/EventsLib.sol index f371c62..613eefe 100644 --- a/src/libraries/EventsLib.sol +++ b/src/libraries/EventsLib.sol @@ -16,15 +16,15 @@ library EventsLib { address indexed sender, address indexed vault, Id indexed supplyMarketId, uint256 suppliedAssets ); - /// @notice Emitted when the owner is set for a vault. - event SetOwner(address indexed sender, address indexed vault, address indexed owner); + /// @notice Emitted when the admin is set for a vault. + event SetAdmin(address indexed sender, address indexed vault, address admin); - /// @notice Emitted when the owner changes the `fee` for a vault. + /// @notice Emitted when the fee is set for a vault. event SetFee(address indexed sender, address indexed vault, uint256 fee); - /// @notice Emitted when the owner transfers the fee for a vault. + /// @notice Emitted when the fee is transfered for a vault. event TransferFee(address indexed sender, address indexed vault, uint256 amount, address indexed feeRecipient); - /// @notice Emitted when the owner updates some flow caps for a vault. + /// @notice Emitted when the flow caps are set for a vault. event SetFlowCaps(address indexed sender, address indexed vault, FlowCapsConfig[] config); } diff --git a/test/PublicAllocatorTest.sol b/test/PublicAllocatorTest.sol index 21812f2..56bad69 100644 --- a/test/PublicAllocatorTest.sol +++ b/test/PublicAllocatorTest.sol @@ -73,20 +73,40 @@ contract PublicAllocatorTest is IntegrationTest { _sortSupplyQueueIdleLast(); } - function testOwner() public { - assertEq(publicAllocator.owner(address(vault)), address(0)); + function testAdmin() public { + assertEq(publicAllocator.admin(address(vault)), address(0)); } - function testSetOwner() public { + function testSetAdmin() public { vm.prank(OWNER); - publicAllocator.setOwner(address(vault), address(1)); - assertEq(publicAllocator.owner(address(vault)), address(1)); + publicAllocator.setAdmin(address(vault), address(1)); + assertEq(publicAllocator.admin(address(vault)), address(1)); } - function testSetOwnerFail() public { + function testSetAdminByAdmin(address sender, address newAdmin) public { + vm.assume(publicAllocator.admin(address(vault)) != sender); + vm.assume(sender != newAdmin); + vm.prank(OWNER); + publicAllocator.setAdmin(address(vault), sender); + vm.prank(sender); + publicAllocator.setAdmin(address(vault), newAdmin); + assertEq(publicAllocator.admin(address(vault)), newAdmin); + } + + function testSetAdminAlreadySet() public { vm.expectRevert(ErrorsLib.AlreadySet.selector); vm.prank(OWNER); - publicAllocator.setOwner(address(vault), address(0)); + publicAllocator.setAdmin(address(vault), address(0)); + } + + function testSetAdminAccessFail(address sender, address newAdmin) public { + vm.assume(sender != OWNER); + vm.assume(publicAllocator.admin(address(vault)) != sender); + vm.assume(publicAllocator.admin(address(vault)) != newAdmin); + + vm.expectRevert(ErrorsLib.NotAdminNorVaultOwner.selector); + vm.prank(sender); + publicAllocator.setAdmin(address(vault), newAdmin); } function testReallocateCapZeroOutflowByDefault(uint128 flow) public { @@ -112,28 +132,28 @@ contract PublicAllocatorTest is IntegrationTest { function testConfigureFlowAccessFail(address sender) public { vm.assume(sender != OWNER); - vm.assume(sender != address(0)); + vm.assume(publicAllocator.admin(address(vault)) != sender); flowCaps.push(FlowCapsConfig(idleParams.id(), FlowCaps(0, 0))); vm.prank(sender); - vm.expectRevert(ErrorsLib.NotOwner.selector); + vm.expectRevert(ErrorsLib.NotAdminNorVaultOwner.selector); publicAllocator.setFlowCaps(address(vault), flowCaps); } function testTransferFeeAccessFail(address sender, address payable recipient) public { vm.assume(sender != OWNER); - vm.assume(sender != address(0)); + vm.assume(publicAllocator.admin(address(vault)) != sender); vm.prank(sender); - vm.expectRevert(ErrorsLib.NotOwner.selector); + vm.expectRevert(ErrorsLib.NotAdminNorVaultOwner.selector); publicAllocator.transferFee(address(vault), recipient); } function testSetFeeAccessFail(address sender, uint256 fee) public { vm.assume(sender != OWNER); - vm.assume(sender != address(0)); + vm.assume(publicAllocator.admin(address(vault)) != sender); vm.prank(sender); - vm.expectRevert(ErrorsLib.NotOwner.selector); + vm.expectRevert(ErrorsLib.NotAdminNorVaultOwner.selector); publicAllocator.setFee(address(vault), fee); } @@ -146,6 +166,18 @@ contract PublicAllocatorTest is IntegrationTest { assertEq(publicAllocator.fee(address(vault)), fee); } + function testSetFeeByAdmin(uint256 fee, address sender) public { + vm.assume(publicAllocator.admin(address(vault)) != sender); + vm.assume(fee != publicAllocator.fee(address(vault))); + vm.prank(OWNER); + publicAllocator.setAdmin(address(vault), sender); + vm.prank(sender); + vm.expectEmit(address(publicAllocator)); + emit EventsLib.SetFee(sender, address(vault), fee); + publicAllocator.setFee(address(vault), fee); + assertEq(publicAllocator.fee(address(vault)), fee); + } + function testSetFeeAlreadySet(uint256 fee) public { vm.assume(fee != publicAllocator.fee(address(vault))); vm.prank(OWNER); @@ -180,6 +212,35 @@ contract PublicAllocatorTest is IntegrationTest { assertEq(flowCap.maxOut, out1); } + function testSetFlowCapsByAdmin(uint128 in0, uint128 out0, uint128 in1, uint128 out1, address sender) public { + vm.assume(publicAllocator.admin(address(vault)) != sender); + in0 = uint128(bound(in0, 0, MAX_SETTABLE_FLOW_CAP)); + out0 = uint128(bound(out0, 0, MAX_SETTABLE_FLOW_CAP)); + in1 = uint128(bound(in1, 0, MAX_SETTABLE_FLOW_CAP)); + out1 = uint128(bound(out1, 0, MAX_SETTABLE_FLOW_CAP)); + + flowCaps.push(FlowCapsConfig(idleParams.id(), FlowCaps(in0, out0))); + flowCaps.push(FlowCapsConfig(allMarkets[0].id(), FlowCaps(in1, out1))); + + vm.prank(OWNER); + publicAllocator.setAdmin(address(vault), sender); + + vm.expectEmit(address(publicAllocator)); + emit EventsLib.SetFlowCaps(sender, address(vault), flowCaps); + + vm.prank(sender); + publicAllocator.setFlowCaps(address(vault), flowCaps); + + FlowCaps memory flowCap; + flowCap = publicAllocator.flowCaps(address(vault), idleParams.id()); + assertEq(flowCap.maxIn, in0); + assertEq(flowCap.maxOut, out0); + + flowCap = publicAllocator.flowCaps(address(vault), allMarkets[0].id()); + assertEq(flowCap.maxIn, in1); + assertEq(flowCap.maxOut, out1); + } + function testPublicReallocateEvent(uint128 flow, address sender) public { flow = uint128(bound(flow, 1, CAP2 / 2)); @@ -298,6 +359,30 @@ contract PublicAllocatorTest is IntegrationTest { assertEq(address(this).balance - before, 2 * 0.001 ether, "wrong fee transferred"); } + function testTransferFeeByAdminSuccess(address sender) public { + vm.assume(publicAllocator.admin(address(vault)) != sender); + vm.prank(OWNER); + publicAllocator.setAdmin(address(vault), sender); + vm.prank(sender); + publicAllocator.setFee(address(vault), 0.001 ether); + + flowCaps.push(FlowCapsConfig(idleParams.id(), FlowCaps(0, 2 ether))); + flowCaps.push(FlowCapsConfig(allMarkets[0].id(), FlowCaps(2 ether, 0))); + vm.prank(OWNER); + publicAllocator.setFlowCaps(address(vault), flowCaps); + withdrawals.push(Withdrawal(idleParams, 1 ether)); + + publicAllocator.reallocateTo{value: 0.001 ether}(address(vault), withdrawals, allMarkets[0]); + publicAllocator.reallocateTo{value: 0.001 ether}(address(vault), withdrawals, allMarkets[0]); + + uint256 before = address(this).balance; + + vm.prank(sender); + publicAllocator.transferFee(address(vault), payable(address(this))); + + assertEq(address(this).balance - before, 2 * 0.001 ether, "wrong fee transferred"); + } + function testTransferFeeFail() public { vm.prank(OWNER); publicAllocator.setFee(address(vault), 0.001 ether);