diff --git a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol index d07d5028..6eb1ad91 100644 --- a/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol +++ b/contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol @@ -6,6 +6,7 @@ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.s import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { BeaconProxy } from "@openzeppelin/contracts/proxy/beacon/BeaconProxy.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; // solhint-disable-next-line max-line-length import { AccessManagedUpgradeable } from "@openzeppelin/contracts-upgradeable/access/manager/AccessManagedUpgradeable.sol"; @@ -97,6 +98,16 @@ contract RoyaltyPolicyLAP is IRoyaltyPolicyLAP, AccessManagedUpgradeable, Reentr $.ipRoyaltyVaultBeacon = beacon; } + /// @dev Upgrades the ip royalty vault beacon + /// @dev Enforced to be only callable by the upgrader admin + /// @param newVault The new ip royalty vault beacon address + function upgradeVaults(address newVault) public restricted { + // UpgradeableBeacon already checks for newImplementation.bytecode.length > 0, + // no need to check for zero address + RoyaltyPolicyLAPStorage storage $ = _getRoyaltyPolicyLAPStorage(); + UpgradeableBeacon($.ipRoyaltyVaultBeacon).upgradeTo(newVault); + } + /// @notice Executes royalty related logic on minting a license /// @dev Enforced to be only callable by RoyaltyModule /// @param ipId The ipId whose license is being minted (licensor) diff --git a/script/foundry/utils/DeployHelper.sol b/script/foundry/utils/DeployHelper.sol index 8620b9b8..4410083b 100644 --- a/script/foundry/utils/DeployHelper.sol +++ b/script/foundry/utils/DeployHelper.sol @@ -140,6 +140,10 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag (bool multisigAdmin, ) = protocolAccessManager.hasRole(ProtocolAdmin.PROTOCOL_ADMIN_ROLE, multisig); (bool multisigUpgrader, ) = protocolAccessManager.hasRole(ProtocolAdmin.UPGRADER_ROLE, multisig); + if (address(royaltyPolicyLAP) != ipRoyaltyVaultBeacon.owner()) { + revert RoleConfigError("RoyaltyPolicyLAP is not owner of IpRoyaltyVaultBeacon"); + } + if (!multisigAdmin || !multisigUpgrader) { revert RoleConfigError("Multisig roles not granted"); } @@ -340,7 +344,8 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag _postdeploy("IpRoyaltyVaultImpl", address(ipRoyaltyVaultImpl)); _predeploy("IpRoyaltyVaultBeacon"); - ipRoyaltyVaultBeacon = new UpgradeableBeacon(address(ipRoyaltyVaultImpl), address(protocolAccessManager)); + // Transfer Ownership to RoyaltyPolicyLAP later + ipRoyaltyVaultBeacon = new UpgradeableBeacon(address(ipRoyaltyVaultImpl), deployer); _postdeploy("IpRoyaltyVaultBeacon", address(ipRoyaltyVaultBeacon)); _predeploy("CoreMetadataModule"); @@ -396,6 +401,7 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag royaltyModule.whitelistRoyaltyToken(address(erc20), true); royaltyPolicyLAP.setSnapshotInterval(7 days); royaltyPolicyLAP.setIpRoyaltyVaultBeacon(address(ipRoyaltyVaultBeacon)); + ipRoyaltyVaultBeacon.transferOwnership(address(royaltyPolicyLAP)); // Dispute Module and SP Dispute Policy address arbitrationRelayer = relayer; @@ -429,11 +435,17 @@ contract DeployHelper is Script, BroadcastManager, JsonDeploymentHandler, Storag ); protocolAccessManager.setTargetFunctionRole(address(licensingModule), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(royaltyModule), selectors, ProtocolAdmin.UPGRADER_ROLE); - protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLAP), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(licenseRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(moduleRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); protocolAccessManager.setTargetFunctionRole(address(ipAssetRegistry), selectors, ProtocolAdmin.UPGRADER_ROLE); + // Royalty and Upgrade Beacon + // Owner of the beacon is the RoyaltyPolicyLAP + selectors = new bytes4[](2); + selectors[0] = RoyaltyPolicyLAP.upgradeVaults.selector; + selectors[1] = UUPSUpgradeable.upgradeToAndCall.selector; + protocolAccessManager.setTargetFunctionRole(address(royaltyPolicyLAP), selectors, ProtocolAdmin.UPGRADER_ROLE); + ///////// Role Granting ///////// protocolAccessManager.grantRole(ProtocolAdmin.UPGRADER_ROLE, multisig, upgraderExecDelay); protocolAccessManager.grantRole(ProtocolAdmin.PROTOCOL_ADMIN_ROLE, multisig, 0); diff --git a/script/foundry/utils/upgrades/ERC7201Helper.s.sol b/script/foundry/utils/upgrades/ERC7201Helper.s.sol index f30036b1..3dc4845c 100644 --- a/script/foundry/utils/upgrades/ERC7201Helper.s.sol +++ b/script/foundry/utils/upgrades/ERC7201Helper.s.sol @@ -12,7 +12,7 @@ import { console2 } from "forge-std/console2.sol"; contract ERC7201HelperScript is Script { string constant NAMESPACE = "story-protocol"; - string constant CONTRACT_NAME = "IPAssetRegistry"; + string constant CONTRACT_NAME = "MockIPRoyaltyVaultV2"; function run() external { bytes memory erc7201Key = abi.encodePacked(NAMESPACE, ".", CONTRACT_NAME); diff --git a/test/foundry/mocks/module/MockIpRoyaltyVaultV2.sol b/test/foundry/mocks/module/MockIpRoyaltyVaultV2.sol new file mode 100644 index 00000000..a706f263 --- /dev/null +++ b/test/foundry/mocks/module/MockIpRoyaltyVaultV2.sol @@ -0,0 +1,36 @@ +import { IpRoyaltyVault } from "contracts/modules/royalty/policies/IpRoyaltyVault.sol"; + +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +contract MockIpRoyaltyVaultV2 is IpRoyaltyVault { + /// @dev Storage structure for the MockIPRoyaltyVaultV2 + /// @custom:storage-location erc7201:story-protocol.MockIPRoyaltyVaultV2 + struct MockIPRoyaltyVaultV2Storage { + string newState; + } + + // keccak256(abi.encode(uint256(keccak256("story-protocol.MockIPRoyaltyVaultV2")) - 1)) & ~bytes32(uint256(0xff)); + bytes32 private constant MockIPRoyaltyVaultV2StorageLocation = + 0x2942176f94974e015a9b06f79a3a2280d18f1872591c134ba237fa184e378300; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor(address royaltyPolicyLAP, address disputeModule) IpRoyaltyVault(royaltyPolicyLAP, disputeModule) { + _disableInitializers(); + } + + function set(string calldata value) external { + _getMockIPRoyaltyVaultV2Storage().newState = value; + } + + function get() external view returns (string memory) { + return _getMockIPRoyaltyVaultV2Storage().newState; + } + + /// @dev Returns the storage struct of MockIPRoyaltyVaultV2. + function _getMockIPRoyaltyVaultV2Storage() private pure returns (MockIPRoyaltyVaultV2Storage storage $) { + assembly { + $.slot := MockIPRoyaltyVaultV2StorageLocation + } + } +} diff --git a/test/foundry/upgrades/IPRoyaltyVaults.t.sol b/test/foundry/upgrades/IPRoyaltyVaults.t.sol new file mode 100644 index 00000000..d74d8bd3 --- /dev/null +++ b/test/foundry/upgrades/IPRoyaltyVaults.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.23; + +import { ProtocolAdmin } from "contracts/lib/ProtocolAdmin.sol"; +import { RoyaltyPolicyLAP } from "contracts/modules/royalty/policies/RoyaltyPolicyLAP.sol"; + +import { BaseTest } from "../utils/BaseTest.t.sol"; + +import { MockIpRoyaltyVaultV2 } from "../mocks/module/MockIpRoyaltyVaultV2.sol"; + +contract IPRoyaltyVaults is BaseTest { + function setUp() public override { + super.setUp(); + vm.prank(u.admin); + protocolAccessManager.grantRole(ProtocolAdmin.UPGRADER_ROLE, u.alice, upgraderExecDelay); + } + + function test_upgradeVaults() public { + address newVault = address(new MockIpRoyaltyVaultV2(address(royaltyPolicyLAP), address(disputeModule))); + (bool immediate, uint32 delay) = protocolAccessManager.canCall( + u.alice, + address(royaltyPolicyLAP), + RoyaltyPolicyLAP.upgradeVaults.selector + ); + assertFalse(immediate); + assertEq(delay, 600); + vm.prank(u.alice); + (bytes32 operationId, uint32 nonce) = protocolAccessManager.schedule( + address(royaltyPolicyLAP), + abi.encodeCall(RoyaltyPolicyLAP.upgradeVaults, (newVault)), + 0 // earliest time possible, upgraderExecDelay + ); + vm.warp(upgraderExecDelay + 1); + + vm.prank(u.alice); + royaltyPolicyLAP.upgradeVaults(newVault); + + assertEq(ipRoyaltyVaultBeacon.implementation(), newVault); + } +}