diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 0f37376..ae399e3 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -9,7 +9,13 @@ ## Overview -The FilBeamOperator contract manages CDN and cache-miss usage reporting and payment settlement for the FilBeam service. This guide covers deployment and migration procedures. +The FilBeamOperator contract manages CDN and cache-miss usage reporting and payment settlement for the FilBeam service. The contract uses the **UUPS proxy pattern** for upgradeability. + +### Architecture +- **Proxy Contract**: The address users interact with (stores all state) +- **Implementation Contract**: Contains the business logic (can be upgraded) + +This guide covers deployment, upgrades, and migration procedures. ## Initial Deployment @@ -39,7 +45,7 @@ The FilBeamOperator contract manages CDN and cache-miss usage reporting and paym ### Deployment Steps -#### Step 1: Deploy New FilBeamOperator Contract +#### Step 1: Deploy FilBeamOperator (Proxy + Implementation) ```bash # Deploy the contract @@ -50,22 +56,27 @@ forge script script/DeployFilBeamOperator.s.sol \ # Expected output: # === FilBeamOperator Deployment Complete === -# FilBeamOperator deployed at: 0x... -# === Configuration === -# FWSS address: -# Payments address: +# === Contract Addresses === +# Proxy Address (use this!): 0x... <-- This is the address you use +# Implementation Address: 0x... <-- For reference only +# === Verification === +# Contract Version: 1.0.0 # ... ``` #### Step 2: Verify Deployment ```bash +# Set the PROXY address (not implementation!) +export FILBEAM_OPERATOR_PROXY_ADDRESS=0x... + # Verify contract configuration -cast call $FILBEAM_OPERATOR_ADDRESS "fwssContractAddress()" --rpc-url $RPC_URL -cast call $FILBEAM_OPERATOR_ADDRESS "paymentsContractAddress()" --rpc-url $RPC_URL -cast call $FILBEAM_OPERATOR_ADDRESS "cdnRatePerByte()" --rpc-url $RPC_URL -cast call $FILBEAM_OPERATOR_ADDRESS "cacheMissRatePerByte()" --rpc-url $RPC_URL -cast call $FILBEAM_OPERATOR_ADDRESS "filBeamOperatorController()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "fwssContractAddress()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "paymentsContractAddress()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "cdnRatePerByte()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "cacheMissRatePerByte()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "filBeamOperatorController()" --rpc-url $RPC_URL +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "version()" --rpc-url $RPC_URL # Should return "1.0.0" ``` #### Step 3: Transfer FWSS Controller Authorization @@ -74,7 +85,7 @@ cast call $FILBEAM_OPERATOR_ADDRESS "filBeamOperatorController()" --rpc-url $RPC # Current FWSS controller should execute this cast send $FWSS_ADDRESS \ "transferFilBeamController(address)" \ - $FILBEAM_OPERATOR_ADDRESS \ + $FILBEAM_OPERATOR_PROXY_ADDRESS \ --private-key $CURRENT_CONTROLLER_PRIVATE_KEY \ --rpc-url $RPC_URL ``` @@ -87,7 +98,7 @@ cast send $FWSS_ADDRESS \ cast call $FWSS_ADDRESS \ "getDataSetInfo(uint256)" \ 1 \ - --from $FILBEAM_OPERATOR_ADDRESS \ + --from $FILBEAM_OPERATOR_PROXY_ADDRESS \ --rpc-url $RPC_URL ``` @@ -111,9 +122,71 @@ cast call $FWSS_ADDRESS \ ## Future Contract Upgrades -### Rate Change Procedure +FilBeamOperator supports two types of upgrades: + +1. **UUPS Upgrade** - For bug fixes or feature additions (preserves rates and state) +2. **Rate Change Migration** - Requires deploying a new proxy (rates are immutable per deployment) + +### UUPS Upgrade (Bug Fixes / Features) + +Use this for logic changes that don't require modifying rates. The preferred method is using the provided upgrade script as it is safer and handles everything atomically. + +#### Method 1: Scripted Upgrade (Preferred) + +The [UpgradeFilBeamOperator.s.sol](./script/UpgradeFilBeamOperator.s.sol) script handles both deploying the new implementation and performing the upgrade in a single atomic broadcast + +```bash +# Required env vars: +# PRIVATE_KEY, FILBEAM_OPERATOR_PROXY_ADDRESS, FWSS_ADDRESS, +# FWSS_STATE_VIEW_ADDRESS, PAYMENTS_ADDRESS, CDN_RATE_PER_BYTE, CACHE_MISS_RATE_PER_BYTE +# Note: Rates are immutable. To keep current rates, query the proxy first: +# cast call $PROXY "cdnRatePerByte()" --rpc-url $RPC_URL +forge script script/UpgradeFilBeamOperator.s.sol \ + --rpc-url $RPC_URL \ + --private-key $OWNER_PRIVATE_KEY +``` + +#### Method 2: Manual Upgrade (Advanced) + +If you prefer manual control or need to separate the deployment and upgrade steps: + +**Step 1: Deploy New Implementation** +```bash +# Deploy only the new implementation contract +forge create src/FilBeamOperator.sol:FilBeamOperator \ + --constructor-args $FWSS_ADDRESS $FWSS_STATE_VIEW_ADDRESS $PAYMENTS_ADDRESS $CDN_RATE_PER_BYTE $CACHE_MISS_RATE_PER_BYTE \ + --rpc-url $RPC_URL \ + --private-key $OWNER_PRIVATE_KEY + +export NEW_IMPLEMENTATION=0x... # From output above +``` + +#### Step 2: Upgrade the Proxy + +```bash +# Owner calls upgradeToAndCall on the proxy +cast send $FILBEAM_OPERATOR_PROXY_ADDRESS \ + "upgradeToAndCall(address,bytes)" \ + $NEW_IMPLEMENTATION \ + "0x" \ + --private-key $OWNER_PRIVATE_KEY \ + --rpc-url $RPC_URL +``` + +#### Step 3: Verify Upgrade + +```bash +# Check new version +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "version()" --rpc-url $RPC_URL +# Should return new version (e.g., "1.1.0") + +# Verify state is preserved +cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "cdnRatePerByte()" --rpc-url $RPC_URL +``` + +### Rate Change Migration -Since rates are immutable in FilBeamOperator, changing rates requires deploying a new contract. FWSS can only have one authorized FilBeamController at a time, which affects settlement capabilities. +Since rates are immutable in FilBeamOperator, changing rates requires deploying a new proxy. FWSS can only have one authorized FilBeamController at a time, which affects settlement capabilities. ### Migration Approach: Clean Transition @@ -190,7 +263,7 @@ If critical issues are discovered after migration: ```bash # If FWSS controller was transferred -cast send $FILBEAM_OPERATOR_ADDRESS \ +cast send $FILBEAM_OPERATOR_PROXY_ADDRESS \ "transferFwssFilBeamController(address)" \ $PREVIOUS_CONTROLLER_ADDRESS \ --private-key $OWNER_PRIVATE_KEY \ diff --git a/README.md b/README.md index cb53d76..a63f36e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ FilBeamOperator is a smart contract used for aggregating CDN and cache-miss usag - **Usage Reporting**: Batch methods for reporting CDN and cache-miss usage - **Rail Settlements**: Independent settlement for CDN and cache-miss payment rails - **Access Control**: Separate roles for contract management and usage reporting +- **Upgradeable**: Uses UUPS proxy pattern for safe contract upgrades ## Foundry @@ -39,15 +40,15 @@ $ forge fmt For full deployment and migration guide refer to the [DEPLOYMENT](./DEPLOYMENT.md) document in this repository. -The FilBeamOperator contract requires the following constructor parameters: +The FilBeamOperator contract uses the UUPS proxy pattern. Deployment creates: +1. **Implementation Contract** - Contains the logic (don't interact with this directly) +2. **Proxy Contract** - The address users interact with (stores all state) + +The contract is initialized with the following parameters: ```solidity -constructor( - address fwssAddress, // FWSS contract address - address _paymentsAddress, // Payments contract address for rail management - uint256 _cdnRatePerByte, // Rate per byte for CDN usage - uint256 _cacheMissRatePerByte, // Rate per byte for cache-miss usage - address _filBeamOperatorController // Address authorized to report usage +function initialize( + address filBeamOperatorController // Address authorized to report usage ) ``` @@ -59,6 +60,7 @@ Deploy the contract using Forge script: PRIVATE_KEY= \ FILBEAM_CONTROLLER= \ FWSS_ADDRESS= \ +FWSS_STATE_VIEW_ADDRESS= \ CDN_PRICE_USD_PER_TIB= \ CACHE_MISS_PRICE_USD_PER_TIB= \ PRICE_DECIMALS= \ @@ -67,6 +69,10 @@ forge script script/DeployFilBeamOperator.s.sol \ --broadcast ``` +The deployment will output: +- **Proxy Address** - Use this address for all interactions +- **Implementation Address** - For reference only + **Note**: The deployer address automatically becomes the contract owner. ## Contract API @@ -104,6 +110,12 @@ function transferOwnership(address newOwner) external onlyOwner function setFilBeamOperatorController(address _filBeamOperatorController) external onlyOwner ``` +**Upgrades (UUPS)** +```solidity +function upgradeToAndCall(address newImplementation, bytes memory data) external onlyOwner +function version() public pure returns (string memory) // Returns current version +``` + ## Key Concepts ### Batch Operations @@ -122,6 +134,12 @@ function setFilBeamOperatorController(address _filBeamOperatorController) extern - **Epoch-Based**: Settlement periods defined by epoch ranges - **Accumulative**: Usage accumulates between settlements +### Upgradeability (UUPS Pattern) +- **Proxy Pattern**: Users interact with a proxy that delegates to an implementation +- **State Preservation**: All data is stored in the proxy and preserved during upgrades +- **Owner-Only Upgrades**: Only the contract owner can authorize upgrades +- **Version Tracking**: Use `version()` to verify the current implementation version + ### Cast ```shell diff --git a/script/DeployFilBeamOperator.s.sol b/script/DeployFilBeamOperator.s.sol index 695fff5..288b3d5 100644 --- a/script/DeployFilBeamOperator.s.sol +++ b/script/DeployFilBeamOperator.s.sol @@ -5,6 +5,7 @@ import "forge-std/Script.sol"; import "../src/FilBeamOperator.sol"; import {FilecoinWarmStorageService} from "@filecoin-services/FilecoinWarmStorageService.sol"; import {FilecoinWarmStorageServiceStateView} from "@filecoin-services/FilecoinWarmStorageServiceStateView.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; interface IERC20 { function decimals() external view returns (uint8); @@ -62,20 +63,30 @@ contract DeployFilBeamOperator is Script { vm.startBroadcast(deployerPrivateKey); - // Deploy the FilBeamOperator contract (deployer becomes owner) - FilBeamOperator filBeam = new FilBeamOperator( - fwssAddress, - fwssStateViewAddress, - paymentsAddress, - cdnRatePerByte, - cacheMissRatePerByte, - filBeamOperatorController + // Step 1: Deploy the implementation + FilBeamOperator implementation = new FilBeamOperator( + fwssAddress, fwssStateViewAddress, paymentsAddress, cdnRatePerByte, cacheMissRatePerByte ); + // Step 2: Encode the initialize call + bytes memory initializeData = abi.encodeCall(FilBeamOperator.initialize, (filBeamOperatorController)); + + //Step 3: Deploy ERC1967 proxy pointing to the implementation + ERC1967Proxy proxy = new ERC1967Proxy(address(implementation), initializeData); + + // Step 4: cast the proxy to FIlBeamOperator for verification + FilBeamOperator filBeam = FilBeamOperator(address(proxy)); + vm.stopBroadcast(); // Log deployment information console2.log("=== FilBeamOperator Deployment Complete ==="); + console2.log("=== Contract Addresses ==="); + console2.log("Proxy Address (use this):", address(proxy)); + console2.log("Implementation Address:", address(implementation)); + console2.log(""); + console2.log("=== Verification ==="); + console2.log("Contract Version:", filBeam.version()); console2.log("FilBeamOperator deployed at:", address(filBeam)); console2.log(""); console2.log("=== Configuration ==="); diff --git a/script/UpgradeFilBeamOperator.s.sol b/script/UpgradeFilBeamOperator.s.sol new file mode 100644 index 0000000..02fe5ae --- /dev/null +++ b/script/UpgradeFilBeamOperator.s.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../src/FilBeamOperator.sol"; + +/** + * @title UpgradeFilBeamOperator + * @dev Upgrades an existing FilBeamOperator proxy to a new implementation + * + * This script deploys a new implementation contract and upgrades the proxy + * State is preserved in the proxy - only the implementation logic changes. + * + * Note: To preserve existing rates, query them from the current proxy: + * cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "cdnRatePerByte()" --rpc-url $RPC_URL + * cast call $FILBEAM_OPERATOR_PROXY_ADDRESS "cacheMissRatePerByte()" --rpc-url $RPC_URL + * + * Required Environment Variables: + * - PRIVATE_KEY: Owner's private key (must be contract owner) + * - FILBEAM_OPERATOR_PROXY_ADDRESS: Address of the existing proxy contract + * - FWSS_ADDRESS: Address of the FWSS contract + * - FWSS_STATE_VIEW_ADDRESS: Address of the FWSS State View contract + * - PAYMENTS_ADDRESS: Address of the Payments contract + * - CDN_RATE_PER_BYTE: CDN rate per byte in USDFC smallest units + * - CACHE_MISS_RATE_PER_BYTE: Cache miss rate per byte in USDFC smallest units + * + * Example usage: + * PRIVATE_KEY=0x... FILBEAM_OPERATOR_PROXY_ADDRESS=0x... FWSS_ADDRESS=0x... FWSS_STATE_VIEW_ADDRESS=0x... PAYMENTS_ADDRESS=0x... CDN_RATE_PER_BYTE=100 CACHE_MISS_RATE_PER_BYTE=200 forge script script/UpgradeFilBeamOperator.s.sol --broadcast + */ +contract UpgradeFilBeamOperator is Script { + function run() public { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + // The address of existing proxy contract + address filBeamOperatorProxyAddress = vm.envAddress("FILBEAM_OPERATOR_PROXY_ADDRESS"); + address fwssAddress = vm.envAddress("FWSS_ADDRESS"); + address fwssStateViewAddress = vm.envAddress("FWSS_STATE_VIEW_ADDRESS"); + address paymentsAddress = vm.envAddress("PAYMENTS_ADDRESS"); + uint256 cdnRatePerByte = vm.envUint("CDN_RATE_PER_BYTE"); + uint256 cacheMissRatePerByte = vm.envUint("CACHE_MISS_RATE_PER_BYTE"); + + vm.startBroadcast(deployerPrivateKey); + + // Step 1: Deploy new implementation contract + FilBeamOperator newImplementation = new FilBeamOperator( + fwssAddress, fwssStateViewAddress, paymentsAddress, cdnRatePerByte, cacheMissRatePerByte + ); + + // Step 2: Upgrade the proxy to point to the new implementation + FilBeamOperator proxy = FilBeamOperator(filBeamOperatorProxyAddress); + proxy.upgradeToAndCall(address(newImplementation), ""); + + vm.stopBroadcast(); + + // Step 3: Verify the upgrade + FilBeamOperator filBeam = FilBeamOperator(filBeamOperatorProxyAddress); + console2.log("=== FilBeamOperator Upgrade Complete ==="); + console2.log("Proxy Address:", filBeamOperatorProxyAddress); + console2.log("New Implementation Address:", address(newImplementation)); + console2.log("Contract Version:", filBeam.version()); + } +} diff --git a/src/FilBeamOperator.sol b/src/FilBeamOperator.sol index 38d3f2d..a47d728 100644 --- a/src/FilBeamOperator.sol +++ b/src/FilBeamOperator.sol @@ -2,12 +2,14 @@ pragma solidity ^0.8.13; import "./Errors.sol"; -import {Ownable, Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {FilecoinPayV1} from "@filecoin-pay/FilecoinPayV1.sol"; import {FilecoinWarmStorageService} from "@filecoin-services/FilecoinWarmStorageService.sol"; import {FilecoinWarmStorageServiceStateView} from "@filecoin-services/FilecoinWarmStorageServiceStateView.sol"; -contract FilBeamOperator is Ownable2Step { +contract FilBeamOperator is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable { struct DataSetUsage { uint256 cdnAmount; uint256 cacheMissAmount; @@ -41,32 +43,42 @@ contract FilBeamOperator is Ownable2Step { event FwssFilBeamControllerChanged(address indexed previousController, address indexed newController); - /// @notice Initializes the FilBeamOperator contract + /// @custom:oz-upgrades-unsafe-allow constructor /// @param _fwssAddress Address of the FWSS contract /// @param _fwssStateViewAddress Address of the FWSS State View Contract /// @param _paymentsAddress Address of the Payments contract /// @param _cdnRatePerByte CDN rate per byte in smallest token units /// @param _cacheMissRatePerByte Cache miss rate per byte in smallest token units - /// @param _filBeamOperatorController Address authorized to record usage and terminate payment rails constructor( address _fwssAddress, address _fwssStateViewAddress, address _paymentsAddress, uint256 _cdnRatePerByte, - uint256 _cacheMissRatePerByte, - address _filBeamOperatorController - ) Ownable(msg.sender) { + uint256 _cacheMissRatePerByte + ) { if (_fwssAddress == address(0)) revert InvalidAddress(); if (_fwssStateViewAddress == address(0)) revert InvalidAddress(); if (_paymentsAddress == address(0)) revert InvalidAddress(); if (_cdnRatePerByte == 0 || _cacheMissRatePerByte == 0) revert InvalidRate(); - if (_filBeamOperatorController == address(0)) revert InvalidAddress(); fwssContractAddress = _fwssAddress; fwssStateViewContractAddress = _fwssStateViewAddress; paymentsContractAddress = _paymentsAddress; cdnRatePerByte = _cdnRatePerByte; cacheMissRatePerByte = _cacheMissRatePerByte; + + _disableInitializers(); + } + + /// @notice Initializes the FilBeamOperator contract + /// @param _filBeamOperatorController Address authorized to record usage and terminate payment rails + function initialize(address _filBeamOperatorController) public initializer { + if (_filBeamOperatorController == address(0)) revert InvalidAddress(); + + __Ownable_init(msg.sender); + __Ownable2Step_init(); + __UUPSUpgradeable_init(); + filBeamOperatorController = _filBeamOperatorController; } @@ -229,4 +241,16 @@ contract FilBeamOperator is Ownable2Step { // Return the minimum of requested amount and available lockup return requestedAmount > rail.lockupFixed ? rail.lockupFixed : requestedAmount; } + + /// @notice Authorizes contract upgrades - only callable by owner + /// @dev Required by UUPS pattern + /// @param newImplementation Address of new implementation contract + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /// @notice Returns the current version of the implementation + /// @dev This version must be incremented manually with each new implementation + /// deployment to allow tracking upgrades on chain. + function version() public pure virtual returns (string memory) { + return "1.0.0"; + } } diff --git a/test/FilBeamOperator.t.sol b/test/FilBeamOperator.t.sol index af4df8f..3115be0 100644 --- a/test/FilBeamOperator.t.sol +++ b/test/FilBeamOperator.t.sol @@ -9,6 +9,7 @@ import {MockFilecoinWarmStorageServiceStateView} from "../src/mocks/MockFilecoin import {MockPayments} from "../src/mocks/MockPayments.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "../src/Errors.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract FilBeamOperatorTest is Test { FilBeamOperator public filBeam; @@ -19,6 +20,8 @@ contract FilBeamOperatorTest is Test { address public filBeamOperatorController; address public user1; address public user2; + FilBeamOperator public implementation; + ERC1967Proxy public proxy; uint256 constant DATA_SET_ID_1 = 1; uint256 constant DATA_SET_ID_2 = 2; @@ -55,16 +58,24 @@ contract FilBeamOperatorTest is Test { mockStateView = new MockFilecoinWarmStorageServiceStateView(); mockPayments = new MockPayments(); - // Deploy FilBeamOperator contract (deployer becomes owner) - filBeam = new FilBeamOperator( + // Deploy implementation + implementation = new FilBeamOperator( address(mockFWSS), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + CACHE_MISS_RATE_PER_BYTE ); + // Encode intialize call + bytes memory initializeData = abi.encodeCall(FilBeamOperator.initialize, (filBeamOperatorController)); + + // Deploy proxy + proxy = new ERC1967Proxy(address(implementation), initializeData); + + // Cast the proxy to FilBeamOperator + filBeam = FilBeamOperator(address(proxy)); + mockFWSS.setAuthorizedCaller(address(filBeam)); // Set up default rails for testing @@ -193,67 +204,41 @@ contract FilBeamOperatorTest is Test { function test_InitializeRevertZeroAddress() public { vm.expectRevert(InvalidAddress.selector); new FilBeamOperator( - address(0), - address(mockStateView), - address(mockPayments), - CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + address(0), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, CACHE_MISS_RATE_PER_BYTE ); vm.expectRevert(InvalidAddress.selector); new FilBeamOperator( - address(mockFWSS), - address(0), - address(mockPayments), - CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + address(mockFWSS), address(0), address(mockPayments), CDN_RATE_PER_BYTE, CACHE_MISS_RATE_PER_BYTE ); vm.expectRevert(InvalidAddress.selector); new FilBeamOperator( - address(mockFWSS), - address(mockStateView), - address(0), - CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + address(mockFWSS), address(mockStateView), address(0), CDN_RATE_PER_BYTE, CACHE_MISS_RATE_PER_BYTE ); } function test_InitializeRevertZeroRate() public { vm.expectRevert(InvalidRate.selector); new FilBeamOperator( - address(mockFWSS), - address(mockStateView), - address(mockPayments), - 0, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + address(mockFWSS), address(mockStateView), address(mockPayments), 0, CACHE_MISS_RATE_PER_BYTE ); vm.expectRevert(InvalidRate.selector); - new FilBeamOperator( - address(mockFWSS), - address(mockStateView), - address(mockPayments), - CDN_RATE_PER_BYTE, - 0, - filBeamOperatorController - ); + new FilBeamOperator(address(mockFWSS), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, 0); } function test_InitializeRevertZeroFilBeamController() public { - vm.expectRevert(InvalidAddress.selector); - new FilBeamOperator( + FilBeamOperator impl = new FilBeamOperator( address(mockFWSS), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - address(0) + CACHE_MISS_RATE_PER_BYTE ); + + vm.expectRevert(InvalidAddress.selector); + new ERC1967Proxy(address(impl), abi.encodeCall(FilBeamOperator.initialize, (address(0)))); } function test_ReportUsageRollup() public { @@ -1407,15 +1392,20 @@ contract FilBeamOperatorTest is Test { function test_TransferFwssFilBeamController_NewOperatorCanCallAfterMigration() public { // Deploy a new FilBeamOperator instance to act as the new operator - FilBeamOperator newOperator = new FilBeamOperator( + FilBeamOperator newOperatorImpl = new FilBeamOperator( address(mockFWSS), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + CACHE_MISS_RATE_PER_BYTE + ); + + ERC1967Proxy newOperatorProxy = new ERC1967Proxy( + address(newOperatorImpl), abi.encodeCall(FilBeamOperator.initialize, (filBeamOperatorController)) ); + FilBeamOperator newOperator = FilBeamOperator(address(newOperatorProxy)); + // Record usage with old operator vm.prank(filBeamOperatorController); filBeam.recordUsageRollups( @@ -1441,15 +1431,20 @@ contract FilBeamOperatorTest is Test { function test_TransferFwssFilBeamController_IntegrationFlow() public { // Deploy new operator - FilBeamOperator newOperator = new FilBeamOperator( + FilBeamOperator newOperatorImpl = new FilBeamOperator( address(mockFWSS), address(mockStateView), address(mockPayments), CDN_RATE_PER_BYTE, - CACHE_MISS_RATE_PER_BYTE, - filBeamOperatorController + CACHE_MISS_RATE_PER_BYTE ); + ERC1967Proxy newOperatorProxy = new ERC1967Proxy( + address(newOperatorImpl), abi.encodeCall(FilBeamOperator.initialize, (filBeamOperatorController)) + ); + + FilBeamOperator newOperator = FilBeamOperator(address(newOperatorProxy)); + // 1. Old operator records usage vm.prank(filBeamOperatorController); filBeam.recordUsageRollups( @@ -1486,4 +1481,63 @@ contract FilBeamOperatorTest is Test { vm.expectRevert(MockFWSS.UnauthorizedCaller.selector); filBeam.settleCDNPaymentRails(_singleUint256Array(DATA_SET_ID_1)); } + + // ============ Upgrade Tests ============ + + function test_CannotInitializeTwice() public { + vm.expectRevert(); + filBeam.initialize(filBeamOperatorController); + } + + function test_CannotInitializeImplementationDirectly() public { + vm.expectRevert(); + implementation.initialize(filBeamOperatorController); + } + + function test_OnlyOwnerCanUpgrade() public { + FilBeamOperator newImpl = new FilBeamOperator( + address(mockFWSS), + address(mockStateView), + address(mockPayments), + CDN_RATE_PER_BYTE, + CACHE_MISS_RATE_PER_BYTE + ); + + vm.prank(user1); + vm.expectRevert(); + filBeam.upgradeToAndCall(address(newImpl), ""); + } + + function test_OwnerCanUpgrade() public { + // Record some state intitally + vm.prank(filBeamOperatorController); + filBeam.recordUsageRollups( + 1, _singleUint256Array(DATA_SET_ID_1), _singleUint256Array(1000), _singleUint256Array(500) + ); + + // Get state before upgrade + (uint256 cdnBefore, uint256 cacheMissBefore, uint256 epochBefore) = filBeam.dataSetUsage(DATA_SET_ID_1); + address ownerBefore = filBeam.owner(); + + // Deploy new implementation + FilBeamOperator newImpl = new FilBeamOperator( + address(mockFWSS), + address(mockStateView), + address(mockPayments), + CDN_RATE_PER_BYTE, + CACHE_MISS_RATE_PER_BYTE + ); + filBeam.upgradeToAndCall(address(newImpl), ""); + + // Get state after upgrade + (uint256 cdnAfter, uint256 cacheMissAfter, uint256 epochAfter) = filBeam.dataSetUsage(DATA_SET_ID_1); + assertEq(cdnBefore, cdnAfter, "CDN amount should be preserved"); + assertEq(cacheMissBefore, cacheMissAfter, "Cache miss amount should be preserved"); + assertEq(epochBefore, epochAfter, "Epoch should be preserved"); + assertEq(ownerBefore, filBeam.owner(), "Owner should be preserved"); + } + + function test_Version() public view { + assertEq(filBeam.version(), "1.0.0"); + } }