From a8afd7d00324fe8554ba042dcffa23a6313c3766 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:04:09 +0100 Subject: [PATCH 01/14] chore: update solhint to accept console.log in scripts --- packages/contracts/.solhint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/contracts/.solhint.json b/packages/contracts/.solhint.json index 31eb531..8cba538 100644 --- a/packages/contracts/.solhint.json +++ b/packages/contracts/.solhint.json @@ -6,6 +6,7 @@ "avoid-sha3": "warn", "compiler-version": ["error", "^0.8.23"], "func-visibility": ["warn", { "ignoreConstructors": true }], - "import-path-check": "off" + "import-path-check": "off", + "no-console": ["off", { "excludedFiles": ["script/**"] }] } } From d9be376918d1393109b0a704a7e6e2f767d9e826 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:09:21 +0100 Subject: [PATCH 02/14] chore: add abstract deployer --- packages/contracts/script/SupDeployer.s.sol | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/contracts/script/SupDeployer.s.sol diff --git a/packages/contracts/script/SupDeployer.s.sol b/packages/contracts/script/SupDeployer.s.sol new file mode 100644 index 0000000..5fa0022 --- /dev/null +++ b/packages/contracts/script/SupDeployer.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { Script, console2 } from "forge-std/Script.sol"; + +abstract contract SupDeployer is Script { + function _startBroadcast() internal returns (address deployer) { + vm.startBroadcast(); + + // This is the way to get deployer address in foundry: + (, deployer,) = vm.readCallers(); + } + + function _stopBroadcast() internal { + vm.stopBroadcast(); + } + + function _showGitRevision() internal { + string[] memory inputs = new string[](2); + inputs[0] = "../tasks/show-git-rev.sh"; + inputs[1] = "forge_ffi_mode"; + try vm.ffi(inputs) returns (bytes memory res) { + console2.log("GIT REVISION : %s", string(res)); + } catch { + console2.log("!! _showGitRevision: FFI not enabled"); + } + } +} From 25fd65d4a33591a14ed53e47dc5a66c06a007f07 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:09:43 +0100 Subject: [PATCH 03/14] feat: add new AddressRegistry and Locker implementation deploy script --- .../script/config/AddressRegistry.sol | 75 ++++++++ .../upgrades/deploy-locker-upgrade.s.sol | 180 ++++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 packages/contracts/script/config/AddressRegistry.sol create mode 100644 packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol new file mode 100644 index 0000000..f1269ff --- /dev/null +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +library AddressRegistry { + struct LockerDeploymentParameters { + address lockerBeacon; + address sup; + address programManager; + address stakingRewardController; + address fontaineBeacon; + address uniswapNonFungiblePositionManager; + address uniswapSupEthxPool; + address uniswapSwapRouter; + address daoTreasury; + bool isUnlockAvailable; + } + + function getLockerDeploymentParameters(uint256 chainId) + internal + pure + returns (LockerDeploymentParameters memory addresses) + { + if (chainId == 8453) { + addresses = getBaseLockerDeploymentParameters(); + } else if (chainId == 84_532) { + addresses = getBaseSepoliaLockerDeploymentParameters(); + } else { + revert("Unsupported chainId"); + } + } + + /** + * @dev Get Base Mainnet address registry + */ + function getBaseLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { + return LockerDeploymentParameters({ + sup: 0xa69f80524381275A7fFdb3AE01c54150644c8792, + programManager: 0x1e32cf099992E9D3b17eDdDFFfeb2D07AED95C6a, + stakingRewardController: 0xb19Ae25A98d352B36CED60F93db926247535048b, + fontaineBeacon: 0xA26FbA47Da24F7DF11b3E4CF60Dcf7D1691Ae47d, + lockerBeacon: 0x664161f0974F5B17FB1fD3FDcE5D1679E829176c, + uniswapNonFungiblePositionManager: 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1, + uniswapSupEthxPool: 0xBa154BEAa14172fF9384B82499732c669527d85D, + uniswapSwapRouter: 0x2626664c2603336E57B271c5C0b26F421741e481, + daoTreasury: 0xac808840f02c47C05507f48165d2222FF28EF4e1, + isUnlockAvailable: true + }); + } + + function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { + return getBaseLockerDeploymentParameters(); + } + + /** + * @dev Get Base Sepolia configuration + */ + function getBaseSepoliaLockerDeploymentParameters() + internal + pure + returns (LockerDeploymentParameters memory addresses) + { + return LockerDeploymentParameters({ + sup: 0xFd62b398DD8a233ad37156690631fb9515059d6A, + programManager: 0x71a1975A1009e48E0BF2f621B6835db5Ea1f7706, + stakingRewardController: 0x9FC0Bb109F3e733Bd84B30F8D89685b0304fC018, + fontaineBeacon: 0xeBfA246A0BAd08A2A3ffB137ed75601AA41867dE, + lockerBeacon: 0xf2880c6D68080393C1784f978417a96ab4f37c38, + uniswapNonFungiblePositionManager: 0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2, + uniswapSupEthxPool: 0xCa2054E3E5A940473DD6dCC4a67ECdfdFa8c0b72, + uniswapSwapRouter: 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4, + daoTreasury: 0xe7143e87661418DEA122941e01Fdb3f9Acfd02aB, + isUnlockAvailable: true + }); + } +} diff --git a/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol new file mode 100644 index 0000000..6113db8 --- /dev/null +++ b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import { console2 } from "forge-std/console2.sol"; +import { SupDeployer } from "script/SupDeployer.s.sol"; +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; +import { FluidLocker } from "src/FluidLocker.sol"; +import { AddressRegistry } from "script/config/AddressRegistry.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { IStakingRewardController } from "src/interfaces/IStakingRewardController.sol"; +import { IEPProgramManager } from "src/interfaces/IEPProgramManager.sol"; +import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; +import { IV3SwapRouter } from "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; + +// forge script script/upgrades/deploy-locker-upgrade.s.sol:DeployLockerUpgrade --ffi --via-ir --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER +contract DeployLockerUpgrade is SupDeployer { + function run() public { + _showGitRevision(); + + uint256 chainId = block.chainid; + + // Get configuration + AddressRegistry.LockerDeploymentParameters memory lockerParams = + AddressRegistry.getLockerDeploymentParameters(chainId); + + // Get the current live Locker Implementation + UpgradeableBeacon lockerBeacon = UpgradeableBeacon(lockerParams.lockerBeacon); + FluidLocker currentLockerImplementation = FluidLocker(payable(lockerBeacon.implementation())); + + // Validate the deployment parameters; + _validateDeploymentParameters(currentLockerImplementation, lockerParams); + + // Start Deployment : + address deployer = _startBroadcast(); + + _logDeploymentParameters(deployer, lockerParams); + + address newFluidLockerImplementation = address( + new FluidLocker( + ISuperToken(lockerParams.sup), + IEPProgramManager(lockerParams.programManager), + IStakingRewardController(lockerParams.stakingRewardController), + lockerParams.fontaineBeacon, + lockerParams.isUnlockAvailable, + INonfungiblePositionManager(lockerParams.uniswapNonFungiblePositionManager), + IUniswapV3Pool(lockerParams.uniswapSupEthxPool), + IV3SwapRouter(lockerParams.uniswapSwapRouter), + lockerParams.daoTreasury + ) + ); + + _logDeploymentResults(newFluidLockerImplementation); + + _stopBroadcast(); + } + + /** + * @notice Returns whether or not a parameter should be checked against current implementation + * @dev if the `paramName` evaluates to true, the check is not necessary + * @param paramName the env var corresponding to a parameter change + * @return shouldCheck true if the param should be check (i.e. no update) false otherwise + */ + function _shouldCheckParam(string memory paramName) internal view returns (bool shouldCheck) { + shouldCheck = !vm.envOr(paramName, false); + } + + /** + * @notice Validates that the new deployment parameters match the current implementation + * @dev Each parameter check can be bypassed by setting the corresponding env var to true. + * This is useful when intentionally updating a specific immutable parameter. + * + * Env vars to bypass checks: + * - UPDATE_SUP: Skip SUP token address validation + * - UPDATE_PROGRAM_MANAGER: Skip EP Program Manager address validation + * - UPDATE_STAKING_REWARD_CONTROLLER: Skip Staking Reward Controller address validation + * - UPDATE_FONTAINE_BEACON: Skip Fontaine Beacon address validation + * - UPDATE_UNLOCK_AVAILABLE: Skip unlock availability flag validation + * - UPDATE_NONFUNGIBLE_POSITION_MANAGER: Skip Uniswap NFT Position Manager address validation + * - UPDATE_ETH_SUP_POOL: Skip ETH/SUP Uniswap V3 Pool address validation + * - UPDATE_SWAP_ROUTER: Skip Uniswap V3 Swap Router address validation + * - UPDATE_DAO_TREASURY: Skip DAO Treasury address validation + * + * @param lockerImpl The current FluidLocker implementation to validate against + * @param lockerParams The address lockerParams containing the new deployment parameters + */ + function _validateDeploymentParameters( + FluidLocker lockerImpl, + AddressRegistry.LockerDeploymentParameters memory lockerParams + ) internal view { + if (_shouldCheckParam("UPDATE_SUP")) { + require(address(lockerImpl.FLUID()) == lockerParams.sup, "SUP is not meant to be updated"); + } + + if (_shouldCheckParam("UPDATE_PROGRAM_MANAGER")) { + require( + address(lockerImpl.EP_PROGRAM_MANAGER()) == lockerParams.programManager, + "ProgramManager is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_STAKING_REWARD_CONTROLLER")) { + require( + address(lockerImpl.STAKING_REWARD_CONTROLLER()) == lockerParams.stakingRewardController, + "StakingRewardController is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_FONTAINE_BEACON")) { + require( + address(lockerImpl.FONTAINE_BEACON()) == lockerParams.fontaineBeacon, + "FontaineBeacon is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_UNLOCK_AVAILABLE")) { + require( + lockerImpl.UNLOCK_AVAILABLE() == lockerParams.isUnlockAvailable, + "UnlockAvailable is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_NONFUNGIBLE_POSITION_MANAGER")) { + require( + address(lockerImpl.NONFUNGIBLE_POSITION_MANAGER()) == lockerParams.uniswapNonFungiblePositionManager, + "NonfungiblePositionManager is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_ETH_SUP_POOL")) { + require( + address(lockerImpl.ETH_SUP_POOL()) == lockerParams.uniswapSupEthxPool, + "EthSupPool is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_SWAP_ROUTER")) { + require( + address(lockerImpl.SWAP_ROUTER()) == lockerParams.uniswapSwapRouter, + "SwapRouter is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_DAO_TREASURY")) { + require(lockerImpl.DAO_TREASURY() == lockerParams.daoTreasury, "DaoTreasury is not meant to be updated"); + } + } + + function _logDeploymentParameters(address deployer, AddressRegistry.LockerDeploymentParameters memory lockerParams) + internal + pure + { + console2.log("DEPLOYING NEW `FluidLocker` IMPLEMENTATION CONTRACT .........."); + console2.log(""); + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SETTINGS *---------------------------------*"); + console2.log("| "); + console2.log("| DEPLOYER : %s", deployer); + console2.log("| SUP_ADDRESS : %s", lockerParams.sup); + console2.log("| PROGRAM_MANAGER_ADDRESS : %s", lockerParams.programManager); + console2.log("| STAKING_REWARD_CONTROLLER_ADDRESS : %s", lockerParams.stakingRewardController); + console2.log("| FONTAINE_BEACON_ADDRESS : %s", lockerParams.fontaineBeacon); + console2.log("| IS_UNLOCK_AVAILABLE : %s", lockerParams.isUnlockAvailable); + console2.log("| DAO_TREASURY_ADDRESS : %s", lockerParams.daoTreasury); + console2.log("| NONFUNGIBLE_POSITION_MANAGER_ADDRESS : %s", lockerParams.uniswapNonFungiblePositionManager); + console2.log("| ETH_SUP_POOL_ADDRESS : %s", lockerParams.uniswapSupEthxPool); + console2.log("| SWAP_ROUTER_ADDRESS : %s", lockerParams.uniswapSwapRouter); + console2.log("| "); + console2.log("*------------------------------------------------------------------------------------------*"); + } + + function _logDeploymentResults(address newFluidLockerImplementation) internal pure { + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SUMMARY *----------------------------------*"); + console2.log("| |"); + console2.log("| FluidLocker (Logic) : deployed at %s |", newFluidLockerImplementation); + console2.log("*------------------------------------------------------------------------------------------*"); + console2.log(""); + } +} From cd9ac643296552b5e9f1a7fd86e0cab9e429efca Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:17:00 +0100 Subject: [PATCH 04/14] chore: added comments --- .../upgrades/deploy-locker-upgrade.s.sol | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol index 6113db8..ab33778 100644 --- a/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol +++ b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol @@ -13,7 +13,45 @@ import { IUniswapV3Pool } from "@uniswap/v3-core/contracts/interfaces/IUniswapV3 import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/interfaces/INonfungiblePositionManager.sol"; import { IV3SwapRouter } from "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; -// forge script script/upgrades/deploy-locker-upgrade.s.sol:DeployLockerUpgrade --ffi --via-ir --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER +/** + * @title DeployLockerUpgrade + * @notice Deployment script for upgrading the FluidLocker implementation + * + * @dev This script deploys a new FluidLocker implementation contract with updated immutable parameters. + * It includes a safety mechanism to prevent accidental parameter changes. + * + * == Deployment Workflow == + * + * 1. Update the desired parameter values in `AddressRegistry.sol` for the target network + * + * 2. For each parameter you intend to change, set the corresponding environment variable to `true`: + * - UPDATE_SUP=true + * - UPDATE_PROGRAM_MANAGER=true + * - UPDATE_STAKING_REWARD_CONTROLLER=true + * - UPDATE_FONTAINE_BEACON=true + * - UPDATE_UNLOCK_AVAILABLE=true + * - UPDATE_NONFUNGIBLE_POSITION_MANAGER=true + * - UPDATE_ETH_SUP_POOL=true + * - UPDATE_SWAP_ROUTER=true + * - UPDATE_DAO_TREASURY=true + * + * 3. Run the deployment script (see command below) + * + * == Safety Mechanism == + * + * The script compares each parameter in `AddressRegistry` against the current live implementation. + * If a parameter differs and the corresponding env var is NOT set, the script reverts. + * This ensures that parameter changes are always intentional and explicitly acknowledged. + * + * == Example == + * + * To deploy an upgrade that only changes the DAO treasury address: + * ``` + * export UPDATE_DAO_TREASURY=true + * forge script script/upgrades/deploy-locker-upgrade.s.sol:DeployLockerUpgrade \ + * --ffi --via-ir --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER + * ``` + */ contract DeployLockerUpgrade is SupDeployer { function run() public { _showGitRevision(); @@ -28,14 +66,16 @@ contract DeployLockerUpgrade is SupDeployer { UpgradeableBeacon lockerBeacon = UpgradeableBeacon(lockerParams.lockerBeacon); FluidLocker currentLockerImplementation = FluidLocker(payable(lockerBeacon.implementation())); - // Validate the deployment parameters; + // Validate the deployment parameters - will revert in case of inexplicit parameter change _validateDeploymentParameters(currentLockerImplementation, lockerParams); - // Start Deployment : + // Start Deployment address deployer = _startBroadcast(); + // Log parameters used for deployment _logDeploymentParameters(deployer, lockerParams); + // Deploy the new Locker Implementation address newFluidLockerImplementation = address( new FluidLocker( ISuperToken(lockerParams.sup), @@ -50,6 +90,7 @@ contract DeployLockerUpgrade is SupDeployer { ) ); + // Log deployment results _logDeploymentResults(newFluidLockerImplementation); _stopBroadcast(); From 02a62708d545c3ceba5de48dc4ee05fc3597ef81 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 30 Jan 2026 09:53:45 +0100 Subject: [PATCH 05/14] feat: added script to deploy factory upgrade --- .../script/config/AddressRegistry.sol | 57 ++++++- .../upgrades/deploy-factory-upgrade.s.sol | 152 ++++++++++++++++++ 2 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 packages/contracts/script/upgrades/deploy-factory-upgrade.s.sol diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol index f1269ff..7579bba 100644 --- a/packages/contracts/script/config/AddressRegistry.sol +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -2,6 +2,13 @@ pragma solidity ^0.8.23; library AddressRegistry { + struct FactoryDeploymentParameters { + address lockerFactory; + address lockerBeacon; + address stakingRewardController; + bool isPaused; + } + struct LockerDeploymentParameters { address lockerBeacon; address sup; @@ -29,6 +36,20 @@ library AddressRegistry { } } + function getFactoryDeploymentParameters(uint256 chainId) + internal + pure + returns (FactoryDeploymentParameters memory params) + { + if (chainId == 8453) { + params = getBaseFactoryDeploymentParameters(); + } else if (chainId == 84_532) { + params = getBaseSepoliaFactoryDeploymentParameters(); + } else { + revert("Unsupported chainId"); + } + } + /** * @dev Get Base Mainnet address registry */ @@ -47,8 +68,16 @@ library AddressRegistry { }); } - function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { - return getBaseLockerDeploymentParameters(); + /** + * @dev Get Base Mainnet Factory deployment parameters + */ + function getBaseFactoryDeploymentParameters() internal pure returns (FactoryDeploymentParameters memory params) { + return FactoryDeploymentParameters({ + lockerFactory: 0xA6694cAB43713287F7735dADc940b555db9d39D9, + lockerBeacon: 0x664161f0974F5B17FB1fD3FDcE5D1679E829176c, + stakingRewardController: 0xb19Ae25A98d352B36CED60F93db926247535048b, + isPaused: false + }); } /** @@ -72,4 +101,28 @@ library AddressRegistry { isUnlockAvailable: true }); } + + /** + * @dev Get Base Sepolia Factory deployment parameters + */ + function getBaseSepoliaFactoryDeploymentParameters() + internal + pure + returns (FactoryDeploymentParameters memory params) + { + return FactoryDeploymentParameters({ + lockerFactory: 0x897D343D24Ac5b84838B976Cf37036EDEfe3E967, + lockerBeacon: 0xf2880c6D68080393C1784f978417a96ab4f37c38, + stakingRewardController: 0x9FC0Bb109F3e733Bd84B30F8D89685b0304fC018, + isPaused: false + }); + } + + function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { + return getBaseLockerDeploymentParameters(); + } + + function getLocalFactoryDeploymentParameters() internal pure returns (FactoryDeploymentParameters memory params) { + return getBaseFactoryDeploymentParameters(); + } } diff --git a/packages/contracts/script/upgrades/deploy-factory-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-factory-upgrade.s.sol new file mode 100644 index 0000000..369f880 --- /dev/null +++ b/packages/contracts/script/upgrades/deploy-factory-upgrade.s.sol @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + * @title DeployLockerFactoryUpgrade + * @notice Deployment script for upgrading the FluidLockerFactory implementation + * + * @dev This script deploys a new FluidLockerFactory implementation contract with updated immutable parameters. + * It includes a safety mechanism to prevent accidental parameter changes. + * + * == Deployment Workflow == + * + * 1. Update the desired parameter values in `AddressRegistry.sol` for the target network + * + * 2. For each parameter you intend to change, set the corresponding environment variable to `true`: + * - UPDATE_LOCKER_BEACON=true + * - UPDATE_STAKING_REWARD_CONTROLLER=true + * - UPDATE_IS_PAUSED=true + * + * 3. Run the deployment script (see command below) + * + * == Safety Mechanism == + * + * The script compares each parameter in `AddressRegistry` against the current live implementation. + * If a parameter differs and the corresponding env var is NOT set, the script reverts. + * This ensures that parameter changes are always intentional and explicitly acknowledged. + * + * == Example == + * + * To deploy an upgrade that only changes the pause status: + * ``` + * export UPDATE_IS_PAUSED=true + * forge script script/upgrades/deploy-factory-upgrade.s.sol:DeployFactoryUpgrade \ + * --ffi --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER + * ``` + */ +import { console2 } from "forge-std/console2.sol"; +import { SupDeployer } from "script/SupDeployer.s.sol"; +import { FluidLockerFactory } from "src/FluidLockerFactory.sol"; +import { AddressRegistry } from "script/config/AddressRegistry.sol"; +import { IStakingRewardController } from "src/interfaces/IStakingRewardController.sol"; + +contract DeployFactoryUpgrade is SupDeployer { + function run() public { + _showGitRevision(); + + uint256 chainId = block.chainid; + + // Get configuration + AddressRegistry.FactoryDeploymentParameters memory factoryParams = + AddressRegistry.getFactoryDeploymentParameters(chainId); + + // Get the current live Factory Implementation + FluidLockerFactory currentFactoryImplementation = FluidLockerFactory(factoryParams.lockerFactory); + + // Validate the deployment parameters - will revert in case of inexplicit parameter change + _validateDeploymentParameters(currentFactoryImplementation, factoryParams); + + // Start Deployment + address deployer = _startBroadcast(); + + // Log parameters used for deployment + _logDeploymentParameters(deployer, factoryParams); + + // Deploy the new Factory Implementation + address newFluidLockerFactoryImplementation = address( + new FluidLockerFactory( + factoryParams.lockerBeacon, + IStakingRewardController(factoryParams.stakingRewardController), + factoryParams.isPaused + ) + ); + + // Log deployment results + _logDeploymentResults(newFluidLockerFactoryImplementation); + + _stopBroadcast(); + } + + /** + * @notice Returns whether or not a parameter should be checked against current implementation + * @dev if the `paramName` evaluates to true, the check is not necessary + * @param paramName the env var corresponding to a parameter change + * @return shouldCheck true if the param should be check (i.e. no update) false otherwise + */ + function _shouldCheckParam(string memory paramName) internal view returns (bool shouldCheck) { + shouldCheck = !vm.envOr(paramName, false); + } + + /** + * @notice Validates that the new deployment parameters match the current implementation + * @dev Each parameter check can be bypassed by setting the corresponding env var to true. + * This is useful when intentionally updating a specific immutable parameter. + * + * Env vars to bypass checks: + * - UPDATE_LOCKER_BEACON: Skip Locker Beacon address validation + * - UPDATE_STAKING_REWARD_CONTROLLER: Skip Staking Reward Controller address validation + * - UPDATE_IS_PAUSED: Skip pause status validation + * + * @param factoryImpl The current FluidLockerFactory implementation to validate against + * @param factoryParams The deployment parameters containing the new values + */ + function _validateDeploymentParameters( + FluidLockerFactory factoryImpl, + AddressRegistry.FactoryDeploymentParameters memory factoryParams + ) internal view { + if (_shouldCheckParam("UPDATE_LOCKER_BEACON")) { + require( + address(factoryImpl.LOCKER_BEACON()) == factoryParams.lockerBeacon, + "LockerBeacon is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_STAKING_REWARD_CONTROLLER")) { + require( + address(factoryImpl.STAKING_REWARD_CONTROLLER()) == factoryParams.stakingRewardController, + "StakingRewardController is not meant to be updated" + ); + } + + if (_shouldCheckParam("UPDATE_IS_PAUSED")) { + require(factoryImpl.IS_PAUSED() == factoryParams.isPaused, "IsPaused is not meant to be updated"); + } + } + + function _logDeploymentParameters( + address deployer, + AddressRegistry.FactoryDeploymentParameters memory factoryParams + ) internal pure { + console2.log("DEPLOYING NEW `FluidLockerFactory` IMPLEMENTATION CONTRACT .........."); + console2.log(""); + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SETTINGS *---------------------------------*"); + console2.log("| "); + console2.log("| DEPLOYER : %s", deployer); + console2.log("| FACTORY_PROXY_ADDRESS : %s", factoryParams.lockerFactory); + console2.log("| LOCKER_BEACON_ADDRESS : %s", factoryParams.lockerBeacon); + console2.log("| STAKING_REWARD_CONTROLLER_ADDRESS : %s", factoryParams.stakingRewardController); + console2.log("| IS_PAUSED : %s", factoryParams.isPaused); + console2.log("| "); + console2.log("*------------------------------------------------------------------------------------------*"); + } + + function _logDeploymentResults(address newFluidLockerFactoryImplementation) internal pure { + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SUMMARY *----------------------------------*"); + console2.log("| |"); + console2.log("| FluidLockerFactory (Logic) : deployed at %s |", newFluidLockerFactoryImplementation); + console2.log("*------------------------------------------------------------------------------------------*"); + console2.log(""); + } +} From 5e995dbb91af2fe99150a570ae9ca92d5e08d05d Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:23:40 +0100 Subject: [PATCH 06/14] feat: added script to deploy StakingRewardController upgrade --- .../script/config/AddressRegistry.sol | 55 ++++++++ ...oy-staking-reward-controller-upgrade.s.sol | 129 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 packages/contracts/script/upgrades/deploy-staking-reward-controller-upgrade.s.sol diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol index 7579bba..3f43c10 100644 --- a/packages/contracts/script/config/AddressRegistry.sol +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -2,6 +2,11 @@ pragma solidity ^0.8.23; library AddressRegistry { + struct StakingRewardControllerDeploymentParameters { + address stakingRewardController; + address sup; + } + struct FactoryDeploymentParameters { address lockerFactory; address lockerBeacon; @@ -50,6 +55,20 @@ library AddressRegistry { } } + function getStakingRewardControllerDeploymentParameters(uint256 chainId) + internal + pure + returns (StakingRewardControllerDeploymentParameters memory params) + { + if (chainId == 8453) { + params = getBaseStakingRewardControllerDeploymentParameters(); + } else if (chainId == 84_532) { + params = getBaseSepoliaStakingRewardControllerDeploymentParameters(); + } else { + revert("Unsupported chainId"); + } + } + /** * @dev Get Base Mainnet address registry */ @@ -80,6 +99,20 @@ library AddressRegistry { }); } + /** + * @dev Get Base Mainnet StakingRewardController deployment parameters + */ + function getBaseStakingRewardControllerDeploymentParameters() + internal + pure + returns (StakingRewardControllerDeploymentParameters memory params) + { + return StakingRewardControllerDeploymentParameters({ + stakingRewardController: 0xb19Ae25A98d352B36CED60F93db926247535048b, + sup: 0xa69f80524381275A7fFdb3AE01c54150644c8792 + }); + } + /** * @dev Get Base Sepolia configuration */ @@ -118,6 +151,20 @@ library AddressRegistry { }); } + /** + * @dev Get Base Sepolia StakingRewardController deployment parameters + */ + function getBaseSepoliaStakingRewardControllerDeploymentParameters() + internal + pure + returns (StakingRewardControllerDeploymentParameters memory params) + { + return StakingRewardControllerDeploymentParameters({ + stakingRewardController: 0x9FC0Bb109F3e733Bd84B30F8D89685b0304fC018, + sup: 0xFd62b398DD8a233ad37156690631fb9515059d6A + }); + } + function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { return getBaseLockerDeploymentParameters(); } @@ -125,4 +172,12 @@ library AddressRegistry { function getLocalFactoryDeploymentParameters() internal pure returns (FactoryDeploymentParameters memory params) { return getBaseFactoryDeploymentParameters(); } + + function getLocalStakingRewardControllerDeploymentParameters() + internal + pure + returns (StakingRewardControllerDeploymentParameters memory params) + { + return getBaseStakingRewardControllerDeploymentParameters(); + } } diff --git a/packages/contracts/script/upgrades/deploy-staking-reward-controller-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-staking-reward-controller-upgrade.s.sol new file mode 100644 index 0000000..012f9c2 --- /dev/null +++ b/packages/contracts/script/upgrades/deploy-staking-reward-controller-upgrade.s.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + * @title DeployStakingRewardControllerUpgrade + * @notice Deployment script for upgrading the StakingRewardController implementation + * + * @dev This script deploys a new StakingRewardController implementation contract with updated immutable parameters. + * It includes a safety mechanism to prevent accidental parameter changes. + * + * == Deployment Workflow == + * + * 1. Update the desired parameter values in `AddressRegistry.sol` for the target network + * + * 2. For each parameter you intend to change, set the corresponding environment variable to `true`: + * - UPDATE_SUP=true + * + * 3. Run the deployment script (see command below) + * + * == Safety Mechanism == + * + * The script compares each parameter in `AddressRegistry` against the current live implementation. + * If a parameter differs and the corresponding env var is NOT set, the script reverts. + * This ensures that parameter changes are always intentional and explicitly acknowledged. + * + * == Example == + * + * To deploy an upgrade that changes the SUP token address: + * ``` + * export UPDATE_SUP=true + * forge script script/upgrades/deploy-staking-reward-controller-upgrade.s.sol:DeployStakingRewardControllerUpgrade \ + * --ffi --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER + * ``` + */ +import { console2 } from "forge-std/console2.sol"; +import { SupDeployer } from "script/SupDeployer.s.sol"; +import { StakingRewardController } from "src/StakingRewardController.sol"; +import { AddressRegistry } from "script/config/AddressRegistry.sol"; +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; + +contract DeployStakingRewardControllerUpgrade is SupDeployer { + function run() public { + _showGitRevision(); + + uint256 chainId = block.chainid; + + // Get configuration + AddressRegistry.StakingRewardControllerDeploymentParameters memory srcParams = + AddressRegistry.getStakingRewardControllerDeploymentParameters(chainId); + + // Get the current live StakingRewardController Implementation + StakingRewardController currentSrcImplementation = StakingRewardController(srcParams.stakingRewardController); + + // Validate the deployment parameters - will revert in case of inexplicit parameter change + _validateDeploymentParameters(currentSrcImplementation, srcParams); + + // Start Deployment + address deployer = _startBroadcast(); + + // Log parameters used for deployment + _logDeploymentParameters(deployer, srcParams); + + // Deploy the new StakingRewardController Implementation + address newStakingRewardControllerImplementation = + address(new StakingRewardController(ISuperToken(srcParams.sup))); + + // Log deployment results + _logDeploymentResults(newStakingRewardControllerImplementation); + + _stopBroadcast(); + } + + /** + * @notice Returns whether or not a parameter should be checked against current implementation + * @dev if the `paramName` evaluates to true, the check is not necessary + * @param paramName the env var corresponding to a parameter change + * @return shouldCheck true if the param should be check (i.e. no update) false otherwise + */ + function _shouldCheckParam(string memory paramName) internal view returns (bool shouldCheck) { + shouldCheck = !vm.envOr(paramName, false); + } + + /** + * @notice Validates that the new deployment parameters match the current implementation + * @dev Each parameter check can be bypassed by setting the corresponding env var to true. + * This is useful when intentionally updating a specific immutable parameter. + * + * Env vars to bypass checks: + * - UPDATE_SUP: Skip SUP token address validation + * + * @param srcImpl The current StakingRewardController implementation to validate against + * @param srcParams The deployment parameters containing the new values + */ + function _validateDeploymentParameters( + StakingRewardController srcImpl, + AddressRegistry.StakingRewardControllerDeploymentParameters memory srcParams + ) internal view { + if (_shouldCheckParam("UPDATE_SUP")) { + require(address(srcImpl.FLUID()) == srcParams.sup, "SUP is not meant to be updated"); + } + } + + function _logDeploymentParameters( + address deployer, + AddressRegistry.StakingRewardControllerDeploymentParameters memory srcParams + ) internal pure { + console2.log("DEPLOYING NEW `StakingRewardController` IMPLEMENTATION CONTRACT .........."); + console2.log(""); + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SETTINGS *---------------------------------*"); + console2.log("| "); + console2.log("| DEPLOYER : %s", deployer); + console2.log("| STAKING_REWARD_CONTROLLER_ADDRESS : %s", srcParams.stakingRewardController); + console2.log("| SUP_ADDRESS : %s", srcParams.sup); + console2.log("| "); + console2.log("*------------------------------------------------------------------------------------------*"); + } + + function _logDeploymentResults(address newStakingRewardControllerImplementation) internal pure { + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SUMMARY *----------------------------------*"); + console2.log("| |"); + console2.log( + "| StakingRewardController (Logic) : deployed at %s |", newStakingRewardControllerImplementation + ); + console2.log("*------------------------------------------------------------------------------------------*"); + console2.log(""); + } +} From a279de27b769adb8eb62b69d234f4e0a2abf945e Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:38:38 +0100 Subject: [PATCH 07/14] feat: added script to deploy StakingRewardController upgrade --- .../script/config/AddressRegistry.sol | 55 ++++++++ .../deploy-program-manager-upgrade.s.sol | 130 ++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 packages/contracts/script/upgrades/deploy-program-manager-upgrade.s.sol diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol index 3f43c10..e94ea1e 100644 --- a/packages/contracts/script/config/AddressRegistry.sol +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -2,6 +2,11 @@ pragma solidity ^0.8.23; library AddressRegistry { + struct ProgramManagerDeploymentParameters { + address programManager; + address taxDistributionPool; + } + struct StakingRewardControllerDeploymentParameters { address stakingRewardController; address sup; @@ -69,6 +74,20 @@ library AddressRegistry { } } + function getProgramManagerDeploymentParameters(uint256 chainId) + internal + pure + returns (ProgramManagerDeploymentParameters memory params) + { + if (chainId == 8453) { + params = getBaseProgramManagerDeploymentParameters(); + } else if (chainId == 84_532) { + params = getBaseSepoliaProgramManagerDeploymentParameters(); + } else { + revert("Unsupported chainId"); + } + } + /** * @dev Get Base Mainnet address registry */ @@ -113,6 +132,20 @@ library AddressRegistry { }); } + /** + * @dev Get Base Mainnet EPProgramManager deployment parameters + */ + function getBaseProgramManagerDeploymentParameters() + internal + pure + returns (ProgramManagerDeploymentParameters memory params) + { + return ProgramManagerDeploymentParameters({ + programManager: 0x1e32cf099992E9D3b17eDdDFFfeb2D07AED95C6a, + taxDistributionPool: 0xF0f494f4BD2C3A6bF8b49E6f798875301d944C0A + }); + } + /** * @dev Get Base Sepolia configuration */ @@ -165,6 +198,20 @@ library AddressRegistry { }); } + /** + * @dev Get Base Sepolia EPProgramManager deployment parameters + */ + function getBaseSepoliaProgramManagerDeploymentParameters() + internal + pure + returns (ProgramManagerDeploymentParameters memory params) + { + return ProgramManagerDeploymentParameters({ + programManager: 0x71a1975A1009e48E0BF2f621B6835db5Ea1f7706, + taxDistributionPool: 0xBed96F4cE618798C286eE8BF7586BD607d491Ce7 + }); + } + function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { return getBaseLockerDeploymentParameters(); } @@ -180,4 +227,12 @@ library AddressRegistry { { return getBaseStakingRewardControllerDeploymentParameters(); } + + function getLocalProgramManagerDeploymentParameters() + internal + pure + returns (ProgramManagerDeploymentParameters memory params) + { + return getBaseProgramManagerDeploymentParameters(); + } } diff --git a/packages/contracts/script/upgrades/deploy-program-manager-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-program-manager-upgrade.s.sol new file mode 100644 index 0000000..52c709a --- /dev/null +++ b/packages/contracts/script/upgrades/deploy-program-manager-upgrade.s.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + * @title DeployEPProgramManagerUpgrade + * @notice Deployment script for upgrading the FluidEPProgramManager implementation + * + * @dev This script deploys a new FluidEPProgramManager implementation contract with updated immutable parameters. + * It includes a safety mechanism to prevent accidental parameter changes. + * + * == Deployment Workflow == + * + * 1. Update the desired parameter values in `AddressRegistry.sol` for the target network + * + * 2. For each parameter you intend to change, set the corresponding environment variable to `true`: + * - UPDATE_TAX_DISTRIBUTION_POOL=true + * + * 3. Run the deployment script (see command below) + * + * == Safety Mechanism == + * + * The script compares each parameter in `AddressRegistry` against the current live implementation. + * If a parameter differs and the corresponding env var is NOT set, the script reverts. + * This ensures that parameter changes are always intentional and explicitly acknowledged. + * + * == Example == + * + * To deploy an upgrade that changes the tax distribution pool address: + * ``` + * export UPDATE_TAX_DISTRIBUTION_POOL=true + * forge script script/upgrades/deploy-program-manager-upgrade.s.sol:DeployProgramManagerUpgrade \ + * --ffi --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER + * ``` + */ +import { console2 } from "forge-std/console2.sol"; +import { SupDeployer } from "script/SupDeployer.s.sol"; +import { FluidEPProgramManager } from "src/FluidEPProgramManager.sol"; +import { AddressRegistry } from "script/config/AddressRegistry.sol"; +import { ISuperfluidPool } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperfluid.sol"; + +contract DeployProgramManagerUpgrade is SupDeployer { + function run() public { + _showGitRevision(); + + uint256 chainId = block.chainid; + + // Get configuration + AddressRegistry.ProgramManagerDeploymentParameters memory pmParams = + AddressRegistry.getProgramManagerDeploymentParameters(chainId); + + // Get the current live EPProgramManager Implementation + FluidEPProgramManager currentPmImplementation = FluidEPProgramManager(pmParams.programManager); + + // Validate the deployment parameters - will revert in case of inexplicit parameter change + _validateDeploymentParameters(currentPmImplementation, pmParams); + + // Start Deployment + address deployer = _startBroadcast(); + + // Log parameters used for deployment + _logDeploymentParameters(deployer, pmParams); + + // Deploy the new EPProgramManager Implementation + address newEPProgramManagerImplementation = + address(new FluidEPProgramManager(ISuperfluidPool(pmParams.taxDistributionPool))); + + // Log deployment results + _logDeploymentResults(newEPProgramManagerImplementation); + + _stopBroadcast(); + } + + /** + * @notice Returns whether or not a parameter should be checked against current implementation + * @dev if the `paramName` evaluates to true, the check is not necessary + * @param paramName the env var corresponding to a parameter change + * @return shouldCheck true if the param should be check (i.e. no update) false otherwise + */ + function _shouldCheckParam(string memory paramName) internal view returns (bool shouldCheck) { + shouldCheck = !vm.envOr(paramName, false); + } + + /** + * @notice Validates that the new deployment parameters match the current implementation + * @dev Each parameter check can be bypassed by setting the corresponding env var to true. + * This is useful when intentionally updating a specific immutable parameter. + * + * Env vars to bypass checks: + * - UPDATE_TAX_DISTRIBUTION_POOL: Skip Tax Distribution Pool address validation + * + * @param epmImpl The current FluidEPProgramManager implementation to validate against + * @param pmParams The deployment parameters containing the new values + */ + function _validateDeploymentParameters( + FluidEPProgramManager epmImpl, + AddressRegistry.ProgramManagerDeploymentParameters memory pmParams + ) internal view { + if (_shouldCheckParam("UPDATE_TAX_DISTRIBUTION_POOL")) { + require( + address(epmImpl.TAX_DISTRIBUTION_POOL()) == pmParams.taxDistributionPool, + "TaxDistributionPool is not meant to be updated" + ); + } + } + + function _logDeploymentParameters( + address deployer, + AddressRegistry.ProgramManagerDeploymentParameters memory pmParams + ) internal pure { + console2.log("DEPLOYING NEW `FluidEPProgramManager` IMPLEMENTATION CONTRACT .........."); + console2.log(""); + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SETTINGS *---------------------------------*"); + console2.log("| "); + console2.log("| DEPLOYER : %s", deployer); + console2.log("| PROGRAM_MANAGER_ADDRESS : %s", pmParams.programManager); + console2.log("| TAX_DISTRIBUTION_POOL_ADDRESS : %s", pmParams.taxDistributionPool); + console2.log("| "); + console2.log("*------------------------------------------------------------------------------------------*"); + } + + function _logDeploymentResults(address newEPProgramManagerImplementation) internal pure { + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SUMMARY *----------------------------------*"); + console2.log("| |"); + console2.log("| FluidEPProgramManager (Logic) : deployed at %s |", newEPProgramManagerImplementation); + console2.log("*------------------------------------------------------------------------------------------*"); + console2.log(""); + } +} From d77b92daa4aebeeddddf6b9bea20e63d9243a8c7 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:44:21 +0100 Subject: [PATCH 08/14] feat: added script to deploy Fontaine upgrade --- .../script/config/AddressRegistry.sol | 56 ++++++++ .../upgrades/deploy-fontaine-upgrade.s.sol | 128 ++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 packages/contracts/script/upgrades/deploy-fontaine-upgrade.s.sol diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol index e94ea1e..871e041 100644 --- a/packages/contracts/script/config/AddressRegistry.sol +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -2,6 +2,11 @@ pragma solidity ^0.8.23; library AddressRegistry { + struct FontaineDeploymentParameters { + address fontaineBeacon; + address sup; + } + struct ProgramManagerDeploymentParameters { address programManager; address taxDistributionPool; @@ -88,6 +93,20 @@ library AddressRegistry { } } + function getFontaineDeploymentParameters(uint256 chainId) + internal + pure + returns (FontaineDeploymentParameters memory params) + { + if (chainId == 8453) { + params = getBaseFontaineDeploymentParameters(); + } else if (chainId == 84_532) { + params = getBaseSepoliaFontaineDeploymentParameters(); + } else { + revert("Unsupported chainId"); + } + } + /** * @dev Get Base Mainnet address registry */ @@ -145,6 +164,20 @@ library AddressRegistry { taxDistributionPool: 0xF0f494f4BD2C3A6bF8b49E6f798875301d944C0A }); } + + /** + * @dev Get Base Mainnet Fontaine deployment parameters + */ + function getBaseFontaineDeploymentParameters() + internal + pure + returns (FontaineDeploymentParameters memory params) + { + return FontaineDeploymentParameters({ + fontaineBeacon: 0xA26FbA47Da24F7DF11b3E4CF60Dcf7D1691Ae47d, + sup: 0xa69f80524381275A7fFdb3AE01c54150644c8792 + }); + } /** * @dev Get Base Sepolia configuration @@ -212,6 +245,21 @@ library AddressRegistry { }); } + + /** + * @dev Get Base Sepolia Fontaine deployment parameters + */ + function getBaseSepoliaFontaineDeploymentParameters() + internal + pure + returns (FontaineDeploymentParameters memory params) + { + return FontaineDeploymentParameters({ + fontaineBeacon: 0xeBfA246A0BAd08A2A3ffB137ed75601AA41867dE, + sup: 0xFd62b398DD8a233ad37156690631fb9515059d6A + }); + } + function getLocalLockerDeploymentParameters() internal pure returns (LockerDeploymentParameters memory addresses) { return getBaseLockerDeploymentParameters(); } @@ -235,4 +283,12 @@ library AddressRegistry { { return getBaseProgramManagerDeploymentParameters(); } + + function getLocalFontaineDeploymentParameters() + internal + pure + returns (FontaineDeploymentParameters memory params) + { + return getBaseFontaineDeploymentParameters(); + } } diff --git a/packages/contracts/script/upgrades/deploy-fontaine-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-fontaine-upgrade.s.sol new file mode 100644 index 0000000..290c178 --- /dev/null +++ b/packages/contracts/script/upgrades/deploy-fontaine-upgrade.s.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/** + * @title DeployFontaineUpgrade + * @notice Deployment script for upgrading the Fontaine implementation + * + * @dev This script deploys a new Fontaine implementation contract with updated immutable parameters. + * It includes a safety mechanism to prevent accidental parameter changes. + * + * == Deployment Workflow == + * + * 1. Update the desired parameter values in `AddressRegistry.sol` for the target network + * + * 2. For each parameter you intend to change, set the corresponding environment variable to `true`: + * - UPDATE_SUP=true + * + * 3. Run the deployment script (see command below) + * + * == Safety Mechanism == + * + * The script compares each parameter in `AddressRegistry` against the current live implementation. + * If a parameter differs and the corresponding env var is NOT set, the script reverts. + * This ensures that parameter changes are always intentional and explicitly acknowledged. + * + * == Example == + * + * To deploy an upgrade that changes the SUP token address: + * ``` + * export UPDATE_SUP=true + * forge script script/upgrades/deploy-fontaine-upgrade.s.sol:DeployFontaineUpgrade \ + * --ffi --rpc-url $BASE_MAINNET_RPC_URL --account SUP_DEPLOYER + * ``` + */ +import { console2 } from "forge-std/console2.sol"; +import { SupDeployer } from "script/SupDeployer.s.sol"; +import { Fontaine } from "src/Fontaine.sol"; +import { AddressRegistry } from "script/config/AddressRegistry.sol"; +import { UpgradeableBeacon } from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import { ISuperToken } from "@superfluid-finance/ethereum-contracts/contracts/interfaces/superfluid/ISuperToken.sol"; + +contract DeployFontaineUpgrade is SupDeployer { + function run() public { + _showGitRevision(); + + uint256 chainId = block.chainid; + + // Get configuration + AddressRegistry.FontaineDeploymentParameters memory fontaineParams = + AddressRegistry.getFontaineDeploymentParameters(chainId); + + // Get the current live Fontaine Implementation from the beacon + UpgradeableBeacon fontaineBeacon = UpgradeableBeacon(fontaineParams.fontaineBeacon); + Fontaine currentFontaineImplementation = Fontaine(fontaineBeacon.implementation()); + + // Validate the deployment parameters - will revert in case of inexplicit parameter change + _validateDeploymentParameters(currentFontaineImplementation, fontaineParams); + + // Start Deployment + address deployer = _startBroadcast(); + + // Log parameters used for deployment + _logDeploymentParameters(deployer, fontaineParams); + + // Deploy the new Fontaine Implementation + address newFontaineImplementation = address(new Fontaine(ISuperToken(fontaineParams.sup))); + + // Log deployment results + _logDeploymentResults(newFontaineImplementation); + + _stopBroadcast(); + } + + /** + * @notice Returns whether or not a parameter should be checked against current implementation + * @dev if the `paramName` evaluates to true, the check is not necessary + * @param paramName the env var corresponding to a parameter change + * @return shouldCheck true if the param should be check (i.e. no update) false otherwise + */ + function _shouldCheckParam(string memory paramName) internal view returns (bool shouldCheck) { + shouldCheck = !vm.envOr(paramName, false); + } + + /** + * @notice Validates that the new deployment parameters match the current implementation + * @dev Each parameter check can be bypassed by setting the corresponding env var to true. + * This is useful when intentionally updating a specific immutable parameter. + * + * Env vars to bypass checks: + * - UPDATE_SUP: Skip SUP token address validation + * + * @param fontaineImpl The current Fontaine implementation to validate against + * @param fontaineParams The deployment parameters containing the new values + */ + function _validateDeploymentParameters( + Fontaine fontaineImpl, + AddressRegistry.FontaineDeploymentParameters memory fontaineParams + ) internal view { + if (_shouldCheckParam("UPDATE_SUP")) { + require(address(fontaineImpl.FLUID()) == fontaineParams.sup, "SUP is not meant to be updated"); + } + } + + function _logDeploymentParameters( + address deployer, + AddressRegistry.FontaineDeploymentParameters memory fontaineParams + ) internal pure { + console2.log("DEPLOYING NEW `Fontaine` IMPLEMENTATION CONTRACT .........."); + console2.log(""); + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SETTINGS *---------------------------------*"); + console2.log("| "); + console2.log("| DEPLOYER : %s", deployer); + console2.log("| FONTAINE_BEACON_ADDRESS : %s", fontaineParams.fontaineBeacon); + console2.log("| SUP_ADDRESS : %s", fontaineParams.sup); + console2.log("| "); + console2.log("*------------------------------------------------------------------------------------------*"); + } + + function _logDeploymentResults(address newFontaineImplementation) internal pure { + console2.log(""); + console2.log("*----------------------------------* DEPLOYMENT SUMMARY *----------------------------------*"); + console2.log("| |"); + console2.log("| Fontaine (Logic) : deployed at %s |", newFontaineImplementation); + console2.log("*------------------------------------------------------------------------------------------*"); + console2.log(""); + } +} From 1019e6dc97d2574db3cde39be579bb708716cef3 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 30 Jan 2026 10:44:50 +0100 Subject: [PATCH 09/14] chore: archive old deploy script --- .../script/upgrades/{ => .archive}/deploy-factory-impl.s.sol | 0 .../contracts/script/upgrades/{ => .archive}/deploy-sip8.s.sol | 0 .../contracts/script/upgrades/{ => .archive}/deploy-tte.s.sol | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/contracts/script/upgrades/{ => .archive}/deploy-factory-impl.s.sol (100%) rename packages/contracts/script/upgrades/{ => .archive}/deploy-sip8.s.sol (100%) rename packages/contracts/script/upgrades/{ => .archive}/deploy-tte.s.sol (100%) diff --git a/packages/contracts/script/upgrades/deploy-factory-impl.s.sol b/packages/contracts/script/upgrades/.archive/deploy-factory-impl.s.sol similarity index 100% rename from packages/contracts/script/upgrades/deploy-factory-impl.s.sol rename to packages/contracts/script/upgrades/.archive/deploy-factory-impl.s.sol diff --git a/packages/contracts/script/upgrades/deploy-sip8.s.sol b/packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol similarity index 100% rename from packages/contracts/script/upgrades/deploy-sip8.s.sol rename to packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol diff --git a/packages/contracts/script/upgrades/deploy-tte.s.sol b/packages/contracts/script/upgrades/.archive/deploy-tte.s.sol similarity index 100% rename from packages/contracts/script/upgrades/deploy-tte.s.sol rename to packages/contracts/script/upgrades/.archive/deploy-tte.s.sol From 34f7556ac2320fa20ae58603fd8f439bed7baa9a Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:20:07 +0200 Subject: [PATCH 10/14] feat: added Agent wallet as co-owner of the reserve --- packages/contracts/src/FluidLocker.sol | 49 +++++++++++++++++-- .../contracts/src/interfaces/IFluidLocker.sol | 16 ++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/packages/contracts/src/FluidLocker.sol b/packages/contracts/src/FluidLocker.sol index a633f27..8272679 100644 --- a/packages/contracts/src/FluidLocker.sol +++ b/packages/contracts/src/FluidLocker.sol @@ -56,6 +56,9 @@ import { TransferHelper } from "@uniswap/v3-periphery/contracts/libraries/Transf import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; import { LiquidityAmounts } from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +/* Solady ECDSA Library */ +import { ECDSA } from "solady/utils/ECDSA.sol"; + using SuperTokenV1Library for ISuperToken; using SafeCast for int256; @@ -161,6 +164,12 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { /// @notice DAO Treasury address used to receive the unlocking fee address public immutable DAO_TREASURY; + /// @notice Agent Wallet Verifier address used to verify the locker owner agent + address public immutable AGENT_WALLET_VERIFIER; + + /// @notice Signature length requirement (r: 32 bytes, s: 32 bytes, v: 1 byte) + uint256 private constant _SIGNATURE_LENGTH = 65; + // _____ __ __ // / ___// /_____ _/ /____ _____ // \__ \/ __/ __ `/ __/ _ \/ ___/ @@ -199,6 +208,15 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { /// @notice Aggregated liquidity balance provided by this locker uint256 private _liquidityBalance; + // _ _______ _____ __ __ + // | | / /__ / / ___// /_____ _/ /____ _____ + // | | / / /_ < \__ \/ __/ __ `/ __/ _ \/ ___/ + // | |/ /___/ / ___/ / /_/ /_/ / /_/ __(__ ) + // |___//____/ /____/\__/\__,_/\__/\___/____/ + + /// @notice This locker owner's agent address + address public lockerOwnerAgent; + // ______ __ __ // / ____/___ ____ _____/ /________ _______/ /_____ _____ // / / / __ \/ __ \/ ___/ __/ ___/ / / / ___/ __/ __ \/ ___/ @@ -225,7 +243,8 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { INonfungiblePositionManager nonfungiblePositionManager, IUniswapV3Pool ethSupPool, IV3SwapRouter swapRouter, - address daoTreasury + address daoTreasury, + address agentWalletVerifier ) { // Disable initializers to prevent implementation contract initalization _disableInitializers(); @@ -245,6 +264,7 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { NONFUNGIBLE_POSITION_MANAGER = nonfungiblePositionManager; ETH_SUP_POOL = ethSupPool; DAO_TREASURY = daoTreasury; + AGENT_WALLET_VERIFIER = agentWalletVerifier; } /** @@ -581,6 +601,27 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { _stake(getAvailableBalance()); } + /// @inheritdoc IFluidLocker + function setLockerOwnerAgent(address agentWallet, bytes calldata signature) external onlyLockerOwner { + if (lockerOwnerAgent != address(0)) { + revert LOCKER_AGENT_ALREADY_SET(); + } + + // Verify the signature format + if (signature.length != _SIGNATURE_LENGTH) { + revert INVALID_SIGNATURE("signature length"); + } + + bytes32 hash = ECDSA.toEthSignedMessageHash(keccak256(abi.encodePacked(msg.sender, agentWallet))); + + // Verify signature validity + if (ECDSA.recover(hash, signature) != AGENT_WALLET_VERIFIER) { + revert INVALID_SIGNATURE("signer"); + } + + lockerOwnerAgent = agentWallet; + } + // _ ___ ______ __ _ // | | / (_)__ _ __ / ____/_ ______ _____/ /_(_)___ ____ _____ // | | / / / _ \ | /| / / / /_ / / / / __ \/ ___/ __/ / __ \/ __ \/ ___/ @@ -602,7 +643,7 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { // WARNING : this call is subject to MEV and Uniswap price manipulation attacks (uint160 sqrtPriceX96,,,,,,) = ETH_SUP_POOL.slot0(); - // Calculate the lower and upper price bounds + // Calculate the lower and upper price bounds // It is assumed that the liquidity positions are full range int24 tickSpacing = ETH_SUP_POOL.tickSpacing(); uint160 sqrtPriceLowerX96 = TickMath.getSqrtRatioAtTick((TickMath.MIN_TICK / tickSpacing) * tickSpacing); @@ -1045,10 +1086,10 @@ contract FluidLocker is Initializable, ReentrancyGuard, IFluidLocker { // /_/ /_/\____/\__,_/_/_/ /_/\___/_/ /____/ /** - * @dev Throws if called by any account other than the owner + * @dev Throws if called by any account other than the owner or their agent */ modifier onlyLockerOwner() { - if (msg.sender != lockerOwner) revert NOT_LOCKER_OWNER(); + if (msg.sender != lockerOwner && msg.sender != lockerOwnerAgent) revert NOT_LOCKER_OWNER(); _; } diff --git a/packages/contracts/src/interfaces/IFluidLocker.sol b/packages/contracts/src/interfaces/IFluidLocker.sol index ce882ed..22ae5cf 100644 --- a/packages/contracts/src/interfaces/IFluidLocker.sol +++ b/packages/contracts/src/interfaces/IFluidLocker.sol @@ -128,6 +128,14 @@ interface IFluidLocker { /// @notice Error thrown when attempting to transfer the unlocking fee to the DAO Treasury fails error FAILED_TO_TRANSFER_UNLOCKING_FEE(); + /// @notice Error thrown when attempting to set a locker agent while one is already set + error LOCKER_AGENT_ALREADY_SET(); + + /// @notice Error thrown when a signature is of invalid + /// @dev Error Selector : 0x30f01ccf + /// @param reason Description of what part of the signature was invalid + error INVALID_SIGNATURE(string reason); + // ______ __ __ ______ __ _ // / ____/ __/ /____ _________ ____ _/ / / ____/_ ______ _____/ /_(_)___ ____ _____ // / __/ | |/_/ __/ _ \/ ___/ __ \/ __ `/ / / /_ / / / / __ \/ ___/ __/ / __ \/ __ \/ ___/ @@ -305,6 +313,14 @@ interface IFluidLocker { bytes memory stackSignature ) external; + /** + * @notice Sets the locker owner agent address + * @dev Only this Locker owner can call this function + * @param agent address of the locker owner agent + * @param signature signature verifying the agent's identity + */ + function setLockerOwnerAgent(address agent, bytes calldata signature) external; + // _ ___ ______ __ _ // | | / (_)__ _ __ / ____/_ ______ _____/ /_(_)___ ____ _____ // | | / / / _ \ | /| / / / /_ / / / / __ \/ ___/ __/ / __ \/ __ \/ ___/ From b9a28b88456f3d7762a04508a0818140e4991f3a Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Thu, 16 Jul 2026 16:20:28 +0200 Subject: [PATCH 11/14] fix: updated test and deploy script to support new constructor --- packages/contracts/script/Deploy.s.sol | 8 ++++++-- packages/contracts/script/config/AddressRegistry.sol | 12 +++++------- .../script/upgrades/.archive/deploy-sip8.s.sol | 4 +++- .../script/upgrades/.archive/deploy-tte.s.sol | 4 +++- .../script/upgrades/deploy-locker-upgrade.s.sol | 3 ++- packages/contracts/test/FluidLocker.t.sol | 4 +++- packages/contracts/test/SFTest.t.sol | 4 +++- .../contracts/test/fork-test/ProdForkTestSip8.t.sol | 3 ++- 8 files changed, 27 insertions(+), 15 deletions(-) diff --git a/packages/contracts/script/Deploy.s.sol b/packages/contracts/script/Deploy.s.sol index f38350e..d29e4c2 100644 --- a/packages/contracts/script/Deploy.s.sol +++ b/packages/contracts/script/Deploy.s.sol @@ -34,6 +34,7 @@ struct DeploySettings { IV3SwapRouter swapRouter; INonfungiblePositionManager nonfungiblePositionManager; IUniswapV3Pool ethSupPool; + address agentWalletVerifier; } function _deployFontaineBeacon(ISuperToken fluid, address governor) @@ -65,7 +66,8 @@ function _deployLockerBeacon( settings.nonfungiblePositionManager, settings.ethSupPool, settings.swapRouter, - settings.treasury + settings.treasury, + settings.agentWalletVerifier ) ); UpgradeableBeacon lockerBeacon = new UpgradeableBeacon(lockerLogicAddress); @@ -206,6 +208,7 @@ contract DeployScript is Script { INonfungiblePositionManager nonfungiblePositionManager = INonfungiblePositionManager(vm.envAddress("NONFUNGIBLE_POSITION_MANAGER_ADDRESS")); IUniswapV3Pool ethSupPool = IUniswapV3Pool(vm.envAddress("ETH_SUP_POOL_ADDRESS")); + address agentWalletVerifier = vm.envAddress("AGENT_WALLET_VERIFIER_ADDRESS"); // Purposedly not enforcing this at contract level in case governance decides to forfeit ownership of the contracts if (governor == address(0)) { @@ -221,7 +224,8 @@ contract DeployScript is Script { unlockStatus: unlockStatus, swapRouter: swapRouter, nonfungiblePositionManager: nonfungiblePositionManager, - ethSupPool: ethSupPool + ethSupPool: ethSupPool, + agentWalletVerifier: agentWalletVerifier }); _logDeploymentSettings(deployer, address(fluid), governor, treasury, factoryPauseStatus, unlockStatus); diff --git a/packages/contracts/script/config/AddressRegistry.sol b/packages/contracts/script/config/AddressRegistry.sol index 871e041..b86d957 100644 --- a/packages/contracts/script/config/AddressRegistry.sol +++ b/packages/contracts/script/config/AddressRegistry.sol @@ -34,6 +34,7 @@ library AddressRegistry { address uniswapSupEthxPool; address uniswapSwapRouter; address daoTreasury; + address agentWalletVerifier; bool isUnlockAvailable; } @@ -121,6 +122,7 @@ library AddressRegistry { uniswapSupEthxPool: 0xBa154BEAa14172fF9384B82499732c669527d85D, uniswapSwapRouter: 0x2626664c2603336E57B271c5C0b26F421741e481, daoTreasury: 0xac808840f02c47C05507f48165d2222FF28EF4e1, + agentWalletVerifier: address(0), // FIXME update with production address isUnlockAvailable: true }); } @@ -164,15 +166,11 @@ library AddressRegistry { taxDistributionPool: 0xF0f494f4BD2C3A6bF8b49E6f798875301d944C0A }); } - + /** * @dev Get Base Mainnet Fontaine deployment parameters */ - function getBaseFontaineDeploymentParameters() - internal - pure - returns (FontaineDeploymentParameters memory params) - { + function getBaseFontaineDeploymentParameters() internal pure returns (FontaineDeploymentParameters memory params) { return FontaineDeploymentParameters({ fontaineBeacon: 0xA26FbA47Da24F7DF11b3E4CF60Dcf7D1691Ae47d, sup: 0xa69f80524381275A7fFdb3AE01c54150644c8792 @@ -197,6 +195,7 @@ library AddressRegistry { uniswapSupEthxPool: 0xCa2054E3E5A940473DD6dCC4a67ECdfdFa8c0b72, uniswapSwapRouter: 0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4, daoTreasury: 0xe7143e87661418DEA122941e01Fdb3f9Acfd02aB, + agentWalletVerifier: address(0), // FIXME update with production address isUnlockAvailable: true }); } @@ -245,7 +244,6 @@ library AddressRegistry { }); } - /** * @dev Get Base Sepolia Fontaine deployment parameters */ diff --git a/packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol b/packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol index 170e9cc..85d8bb2 100644 --- a/packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol +++ b/packages/contracts/script/upgrades/.archive/deploy-sip8.s.sol @@ -169,6 +169,7 @@ contract DeploySIP8_PART_II is DeploySIP8 { IUniswapV3Pool ethSupPool = IUniswapV3Pool(vm.envAddress("ETH_SUP_POOL_ADDRESS")); IV3SwapRouter swapRouter = IV3SwapRouter(vm.envAddress("SWAP_ROUTER_ADDRESS")); address daoTreasury = vm.envAddress("DAO_TREASURY_ADDRESS"); + address agentWalletVerifier = vm.envAddress("AGENT_WALLET_VERIFIER_ADDRESS"); // Start Deployment : address deployer = _startBroadcast(); @@ -183,7 +184,8 @@ contract DeploySIP8_PART_II is DeploySIP8 { nonfungiblePositionManager, ethSupPool, swapRouter, - daoTreasury + daoTreasury, + agentWalletVerifier ) ); diff --git a/packages/contracts/script/upgrades/.archive/deploy-tte.s.sol b/packages/contracts/script/upgrades/.archive/deploy-tte.s.sol index 50d114f..8dc8ad8 100644 --- a/packages/contracts/script/upgrades/.archive/deploy-tte.s.sol +++ b/packages/contracts/script/upgrades/.archive/deploy-tte.s.sol @@ -161,6 +161,7 @@ contract DeployTTE_PART_II is DeployTTE { IUniswapV3Pool ethSupPool = IUniswapV3Pool(vm.envAddress("ETH_SUP_POOL_ADDRESS")); IV3SwapRouter swapRouter = IV3SwapRouter(vm.envAddress("SWAP_ROUTER_ADDRESS")); address daoTreasury = vm.envAddress("DAO_TREASURY_ADDRESS"); + address agentWalletVerifier = vm.envAddress("AGENT_WALLET_VERIFIER_ADDRESS"); // Start Deployment : address deployer = _startBroadcast(); @@ -175,7 +176,8 @@ contract DeployTTE_PART_II is DeployTTE { nonfungiblePositionManager, ethSupPool, swapRouter, - daoTreasury + daoTreasury, + agentWalletVerifier ) ); diff --git a/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol index ab33778..9c4466b 100644 --- a/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol +++ b/packages/contracts/script/upgrades/deploy-locker-upgrade.s.sol @@ -86,7 +86,8 @@ contract DeployLockerUpgrade is SupDeployer { INonfungiblePositionManager(lockerParams.uniswapNonFungiblePositionManager), IUniswapV3Pool(lockerParams.uniswapSupEthxPool), IV3SwapRouter(lockerParams.uniswapSwapRouter), - lockerParams.daoTreasury + lockerParams.daoTreasury, + lockerParams.agentWalletVerifier ) ); diff --git a/packages/contracts/test/FluidLocker.t.sol b/packages/contracts/test/FluidLocker.t.sol index 0c897d2..5ad4391 100644 --- a/packages/contracts/test/FluidLocker.t.sol +++ b/packages/contracts/test/FluidLocker.t.sol @@ -910,7 +910,8 @@ contract FluidLockerTTETest is FluidLockerBaseTest { _nonfungiblePositionManager, _pool, _swapRouter, - FLUID_TREASURY + FLUID_TREASURY, + AGENT_WALLET_VERIFIER ) ); @@ -1961,6 +1962,7 @@ contract FluidLockerLayoutTest is FluidLocker { INonfungiblePositionManager(address(0)), IUniswapV3Pool(address(0)), IV3SwapRouter(address(0)), + address(0), address(0) ) { } diff --git a/packages/contracts/test/SFTest.t.sol b/packages/contracts/test/SFTest.t.sol index f1534c5..b4686a4 100644 --- a/packages/contracts/test/SFTest.t.sol +++ b/packages/contracts/test/SFTest.t.sol @@ -64,6 +64,7 @@ contract SFTest is Test { address public constant CAROL = address(0x3); address public constant FLUID_TREASURY = address(0x4); address[] internal TEST_ACCOUNTS = [ADMIN, FLUID_TREASURY, ALICE, BOB, CAROL]; + address public constant AGENT_WALLET_VERIFIER = address(0x5); TestToken internal _fluidUnderlying; SuperToken internal _fluidSuperToken; @@ -170,7 +171,8 @@ contract SFTest is Test { unlockStatus: LOCKER_CAN_UNLOCK, swapRouter: _swapRouter, nonfungiblePositionManager: _nonfungiblePositionManager, - ethSupPool: _pool + ethSupPool: _pool, + agentWalletVerifier: AGENT_WALLET_VERIFIER }); vm.startPrank(ADMIN); diff --git a/packages/contracts/test/fork-test/ProdForkTestSip8.t.sol b/packages/contracts/test/fork-test/ProdForkTestSip8.t.sol index ab88514..df958f9 100644 --- a/packages/contracts/test/fork-test/ProdForkTestSip8.t.sol +++ b/packages/contracts/test/fork-test/ProdForkTestSip8.t.sol @@ -271,7 +271,8 @@ contract ProdForkTestSip8 is Test { _nonfungiblePositionManager, IUniswapV3Pool(address(0)), _swapRouter, - _DAO_MULTISIG + _DAO_MULTISIG, + address(0) ) ); vm.stopPrank(); From 2dbfb424b03a0d279fe41c5f9807400f507f93f7 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:34:12 +0200 Subject: [PATCH 12/14] feat: update fork test (per latest change of SPR contract owner) --- packages/contracts/test/fork-test/ProdForkTest.t.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/contracts/test/fork-test/ProdForkTest.t.sol b/packages/contracts/test/fork-test/ProdForkTest.t.sol index eb307a0..16850f7 100644 --- a/packages/contracts/test/fork-test/ProdForkTest.t.sol +++ b/packages/contracts/test/fork-test/ProdForkTest.t.sol @@ -34,6 +34,7 @@ contract ProdForkTest is Test { uint96 internal constant _SIGNER_PKEY = 69_420; address internal constant _DAO_MULTISIG = 0xac808840f02c47C05507f48165d2222FF28EF4e1; + address internal constant _SPR_OPS_MULTISIG = 0x2cC02D08ad4541f686254b7048f0cC0c45aB1c55; address internal constant _ALICE = address(0x1); function setUp() public { @@ -55,16 +56,16 @@ contract ProdForkTest is Test { uint32 duration = 90 days; /// @dev Create the program - vm.prank(_DAO_MULTISIG); + vm.prank(_SPR_OPS_MULTISIG); ISuperfluidPool pool = _programManager.createProgram( - programId, _DAO_MULTISIG, vm.addr(_SIGNER_PKEY), _sup, "PROGRAM 1", "PROGRAM_1" + programId, _SPR_OPS_MULTISIG, vm.addr(_SIGNER_PKEY), _sup, "PROGRAM 1", "PROGRAM_1" ); /// @dev Grant units to Alice _helperGrantUnitsToAlice(programId, 1); /// @dev Approve and set flow permissions - vm.startPrank(_DAO_MULTISIG); + vm.startPrank(_SPR_OPS_MULTISIG); _macroForwarder.runMacro( IUserDefinedMacro(address(_programManager)), _programManager.paramsGivePermission(programId, fundingAmount, duration) From 261bb3c4fb57712cb02593e285aa06f46e0c7eda Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:41:13 +0200 Subject: [PATCH 13/14] fix: added fuzz param checks --- packages/contracts/test/SFTest.t.sol | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/contracts/test/SFTest.t.sol b/packages/contracts/test/SFTest.t.sol index b4686a4..373ca6d 100644 --- a/packages/contracts/test/SFTest.t.sol +++ b/packages/contracts/test/SFTest.t.sol @@ -64,7 +64,8 @@ contract SFTest is Test { address public constant CAROL = address(0x3); address public constant FLUID_TREASURY = address(0x4); address[] internal TEST_ACCOUNTS = [ADMIN, FLUID_TREASURY, ALICE, BOB, CAROL]; - address public constant AGENT_WALLET_VERIFIER = address(0x5); + uint256 public constant AGENT_WALLET_VERIFIER_PKEY = 0xA6E47; + address public immutable AGENT_WALLET_VERIFIER = vm.addr(AGENT_WALLET_VERIFIER_PKEY); TestToken internal _fluidUnderlying; SuperToken internal _fluidSuperToken; @@ -257,6 +258,17 @@ contract SFTest is Test { signature = abi.encodePacked(r, s, v); } + function _helperGenerateAgentSignature(uint256 _signerPkey, address _lockerOwner, address _agentWallet) + internal + pure + returns (bytes memory signature) + { + bytes32 digest = keccak256(abi.encodePacked(_lockerOwner, _agentWallet)).toEthSignedMessageHash(); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPkey, digest); + signature = abi.encodePacked(r, s, v); + } + function _helperDistributeToProgramPool(uint256 programId, uint256 amount, uint256 period) internal returns (int96 actualDistributionFlowRate) From 281f6601c66704e36d8e6689a4f5cea1bd7ecec8 Mon Sep 17 00:00:00 2001 From: Pilou <76021631+0xPilou@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:01:18 +0200 Subject: [PATCH 14/14] feat: added dual locker owner tests --- packages/contracts/test/FluidLocker.t.sol | 346 +++++++++++++++++++++- 1 file changed, 345 insertions(+), 1 deletion(-) diff --git a/packages/contracts/test/FluidLocker.t.sol b/packages/contracts/test/FluidLocker.t.sol index 5ad4391..1522218 100644 --- a/packages/contracts/test/FluidLocker.t.sol +++ b/packages/contracts/test/FluidLocker.t.sol @@ -24,6 +24,7 @@ import { INonfungiblePositionManager } from "@uniswap/v3-periphery/contracts/int import { IV3SwapRouter } from "@uniswap/swap-router-contracts/contracts/interfaces/IV3SwapRouter.sol"; import { LiquidityAmounts } from "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; import { TickMath } from "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import { ECDSA } from "solady/utils/ECDSA.sol"; using SuperTokenV1Library for ISuperToken; using SafeCast for int256; @@ -1775,6 +1776,9 @@ contract FluidLockerTTETest is FluidLockerBaseTest { function testV2withdrawDustETH(address _nonLockerOwner, uint256 ethAmount) external { vm.assume(_nonLockerOwner != ALICE); + vm.assume(_nonLockerOwner != address(0)); + vm.assume(_nonLockerOwner != FluidLocker(payable(address(aliceLocker))).lockerOwnerAgent()); + ethAmount = uint256(bound(ethAmount, 1, 1_000_000_000 ether)); _helperUpgradeLocker(); @@ -1951,12 +1955,339 @@ contract FluidLockerTTETest is FluidLockerBaseTest { } } +contract FluidLockerAgentTest is FluidLockerBaseTest { + /// @dev secp256k1 curve order (upper bound for valid private keys) + uint256 internal constant _SECP256K1_ORDER = + 115792089237316195423570985008687907852837564279074904382605163141518161494337; + + address internal AGENT; + + function setUp() public virtual override { + super.setUp(); + AGENT = makeAddr("agent"); + } + + function _helperSetLockerOwnerAgent(IFluidLocker locker, address owner, address agentWallet) internal { + bytes memory signature = _helperGenerateAgentSignature(AGENT_WALLET_VERIFIER_PKEY, owner, agentWallet); + + vm.prank(owner); + locker.setLockerOwnerAgent(agentWallet, signature); + } + + // _____ __ __ __ ____ ___ __ + // / ___/___ / /_/ / ____ ______/ /_____ _____ / __ \_ ______ ___ ____/ _ | ____ ____ ____ / /_ + // \__ \/ _ \/ __/ / / __ \/ ___/ //_/ _ \/ ___/ / / / / | /| / / __ \/ _ \/ ___/ __ |/ __ `/ _ \/ __ \/ __/ + // ___/ / __/ /_/ /___/ /_/ / /__/ ,< / __/ / / /_/ /| |/ |/ / / / / __/ / / / | / /_/ / __/ / / / /_ + // /____/\___/\__/_____/\____/\___/_/|_|\___/_/ \____/ |__/|__/_/ /_/\___/_/ /_/ |_\__, /\___/_/ /_/\__/ + // /____/ + + function testSetLockerOwnerAgent(address agentWallet) external { + vm.assume(agentWallet != address(0)); + + assertEq( + FluidLocker(payable(address(aliceLocker))).lockerOwnerAgent(), address(0), "agent should not be set yet" + ); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, agentWallet); + + assertEq(FluidLocker(payable(address(aliceLocker))).lockerOwnerAgent(), agentWallet, "agent not correctly set"); + } + + function testSetLockerOwnerAgent_notLockerOwner(address notOwner) external { + vm.assume(notOwner != ALICE && notOwner != address(0)); + + // Even a valid verifier-signed signature does not allow a third party to set the agent + bytes memory signature = _helperGenerateAgentSignature(AGENT_WALLET_VERIFIER_PKEY, ALICE, AGENT); + + vm.prank(notOwner); + vm.expectRevert(IFluidLocker.NOT_LOCKER_OWNER.selector); + aliceLocker.setLockerOwnerAgent(AGENT, signature); + } + + function testSetLockerOwnerAgent_agentAlreadySet() external { + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + + address newAgent = makeAddr("newAgent"); + bytes memory signature = _helperGenerateAgentSignature(AGENT_WALLET_VERIFIER_PKEY, ALICE, newAgent); + + // The owner cannot rotate the agent once set + vm.prank(ALICE); + vm.expectRevert(IFluidLocker.LOCKER_AGENT_ALREADY_SET.selector); + aliceLocker.setLockerOwnerAgent(newAgent, signature); + + // The agent cannot replace itself either + vm.prank(AGENT); + vm.expectRevert(IFluidLocker.LOCKER_AGENT_ALREADY_SET.selector); + aliceLocker.setLockerOwnerAgent(newAgent, signature); + + assertEq(FluidLocker(payable(address(aliceLocker))).lockerOwnerAgent(), AGENT, "agent should be unchanged"); + } + + function testSetLockerOwnerAgent_invalidSignatureLength(bytes memory signature) external { + vm.assume(signature.length != 65); + + vm.prank(ALICE); + vm.expectRevert(abi.encodeWithSelector(IFluidLocker.INVALID_SIGNATURE.selector, "signature length")); + aliceLocker.setLockerOwnerAgent(AGENT, signature); + } + + function testSetLockerOwnerAgent_invalidSigner(uint256 attackerPkey) external { + attackerPkey = bound(attackerPkey, 1, _SECP256K1_ORDER - 1); + vm.assume(attackerPkey != AGENT_WALLET_VERIFIER_PKEY); + + bytes memory signature = _helperGenerateAgentSignature(attackerPkey, ALICE, AGENT); + + vm.prank(ALICE); + vm.expectRevert(abi.encodeWithSelector(IFluidLocker.INVALID_SIGNATURE.selector, "signer")); + aliceLocker.setLockerOwnerAgent(AGENT, signature); + } + + function testSetLockerOwnerAgent_malformedSignature() external { + // 65 zero-bytes : correct length but unrecoverable (Solady ECDSA reverts with `InvalidSignature()`) + bytes memory signature = new bytes(65); + + vm.prank(ALICE); + vm.expectRevert(ECDSA.InvalidSignature.selector); + aliceLocker.setLockerOwnerAgent(AGENT, signature); + } + + function testSetLockerOwnerAgent_signatureNotReplayableAcrossOwners() external { + // A signature issued for ALICE cannot be replayed by BOB on his own locker + bytes memory signature = _helperGenerateAgentSignature(AGENT_WALLET_VERIFIER_PKEY, ALICE, AGENT); + + vm.prank(BOB); + vm.expectRevert(abi.encodeWithSelector(IFluidLocker.INVALID_SIGNATURE.selector, "signer")); + bobLocker.setLockerOwnerAgent(AGENT, signature); + } + + function testSetLockerOwnerAgent_signatureBoundToAgent() external { + // A signature issued for a given agent wallet cannot be used to set a different agent wallet + address otherAgent = makeAddr("otherAgent"); + bytes memory signature = _helperGenerateAgentSignature(AGENT_WALLET_VERIFIER_PKEY, ALICE, otherAgent); + + vm.prank(ALICE); + vm.expectRevert(abi.encodeWithSelector(IFluidLocker.INVALID_SIGNATURE.selector, "signer")); + aliceLocker.setLockerOwnerAgent(AGENT, signature); + } + + // ___ __ ____ __ _ + // / | ____ ____ ____ / /_ / __ \____ ___ _________ _/ /_(_)___ ____ _____ + // / /| |/ __ `/ _ \/ __ \/ __/ / / / / __ \/ _ \/ ___/ __ `/ __/ / __ \/ __ \/ ___/ + // / ___ / /_/ / __/ / / / /_ / /_/ / /_/ / __/ / / /_/ / /_/ / /_/ / / / (__ ) + // /_/ |_\__, /\___/_/ /_/\__/ \____/ .___/\___/_/ \__,_/\__/_/\____/_/ /_/____/ + // /____/ /_/ + + function testAgentCanStakeAndUnstake(uint256 amountToStake) external { + amountToStake = bound(amountToStake, 1 ether, 100_000_000 ether); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + _helperFundLocker(address(aliceLocker), amountToStake); + + vm.prank(AGENT); + aliceLocker.stake(amountToStake); + + assertEq(aliceLocker.getStakedBalance(), amountToStake, "Staked balance should be `amountToStake`"); + assertEq(aliceLocker.getAvailableBalance(), 0, "Available balance should be 0"); + + vm.prank(AGENT); + vm.expectRevert(IFluidLocker.STAKING_COOLDOWN_NOT_ELAPSED.selector); + aliceLocker.unstake(amountToStake); + + vm.warp(block.timestamp + _STAKING_COOLDOWN_PERIOD); + + vm.prank(AGENT); + aliceLocker.unstake(amountToStake); + + assertEq(aliceLocker.getStakedBalance(), 0, "Staked balance should be 0"); + assertEq(aliceLocker.getAvailableBalance(), amountToStake, "Available balance should be `amountToStake`"); + } + + function testAgentCanClaimAndStake(uint256 units, uint256 amountToStake) external { + units = bound(units, 1, 1_000_000); + amountToStake = bound(amountToStake, 1 ether, 100_000_000 ether); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + + // The stack signature remains bound to the locker owner (ALICE), not the agent + uint256 nonce = _programManager.getNextValidNonce(PROGRAM_0, ALICE); + bytes memory signature = _helperGenerateSignature(signerPkey, ALICE, units, PROGRAM_0, nonce); + + _helperFundLocker(address(aliceLocker), amountToStake); + + vm.prank(AGENT); + aliceLocker.claimAndStake(PROGRAM_0, units, nonce, signature); + + assertEq(aliceLocker.getStakedBalance(), amountToStake, "Staked balance should be `amountToStake`"); + assertEq(programPools[0].getUnits(address(aliceLocker)), units, "units not updated"); + } + + function testAgentCanConnectAndDisconnect(uint256 units) external { + units = bound(units, 1, 1_000_000); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + + uint256 nonce = _programManager.getNextValidNonce(PROGRAM_0, ALICE); + bytes memory signature = _helperGenerateSignature(signerPkey, ALICE, units, PROGRAM_0, nonce); + + vm.prank(BOB); + _programManager.updateUserUnits(ALICE, PROGRAM_0, units, nonce, signature); + + vm.prank(AGENT); + aliceLocker.connect(PROGRAM_0); + + assertEq( + _fluid.isMemberConnected(address(programPools[0]), address(aliceLocker)), + true, + "locker should be connected to the program pool" + ); + + vm.prank(AGENT); + aliceLocker.disconnect(PROGRAM_0); + + assertEq( + _fluid.isMemberConnected(address(programPools[0]), address(aliceLocker)), + false, + "locker should be disconnected from the program pool" + ); + } + + function testAgentCanInstantUnlock(uint256 unlockAmount) external { + unlockAmount = bound(unlockAmount, 10e18, 1_000_000e18); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + _helperFundLocker(address(aliceLocker), unlockAmount); + + // Ensure the tax distribution pools have units + _helperLockerStake(address(bobLocker)); + _helperLockerProvideLiquidity(address(carolLocker)); + + uint256 unlockingFee = FluidLocker(payable(address(aliceLocker))).UNLOCKING_FEE(); + vm.deal(AGENT, unlockingFee); + + uint256 aliceBalanceBefore = _fluidSuperToken.balanceOf(ALICE); + + (uint256 amountToUser,,) = _helperCalculateTaxDisitrutionInstantUnlock(unlockAmount); + + // The agent can trigger the unlock but funds are only directed via the explicit recipient argument + vm.prank(AGENT); + aliceLocker.unlock{ value: unlockingFee }(unlockAmount, 0, ALICE); + + assertEq( + _fluidSuperToken.balanceOf(ALICE), + aliceBalanceBefore + amountToUser, + "incorrect ALICE balance after agent-triggered instant unlock" + ); + assertEq(_fluidSuperToken.balanceOf(AGENT), 0, "agent should not receive any SUP"); + } + + function testAgentCanProvideAndWithdrawLiquidity(uint256 ethAmount) external { + ethAmount = bound(ethAmount, 0.001 ether, 1000 ether); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + + uint256 supAmountToLP = ethAmount * 20_000 * 9900 / 10_000; + _helperFundLocker(address(aliceLocker), supAmountToLP); + + vm.deal(AGENT, ethAmount); + + vm.prank(AGENT); + aliceLocker.provideLiquidity{ value: ethAmount }(supAmountToLP); + + uint256 positionCount = FluidLocker(payable(address(aliceLocker))).activePositionCount(); + assertEq(positionCount, 1, "position count should be 1"); + + uint256 positionTokenId = + _nonfungiblePositionManager.tokenOfOwnerByIndex(address(aliceLocker), positionCount - 1); + + (,,,,,,, uint128 positionLiquidity,,,,) = _nonfungiblePositionManager.positions(positionTokenId); + (uint256 amount0ToRemove, uint256 amount1ToRemove) = _helperGetAmountsForLiquidity(_pool, positionLiquidity); + + vm.warp(uint256(FluidLocker(payable(address(aliceLocker))).lpCooldownTimestamps(positionTokenId))); + + uint256 aliceEthBalanceBefore = ALICE.balance; + uint256 agentEthBalanceBefore = AGENT.balance; + + vm.prank(AGENT); + aliceLocker.withdrawLiquidity(positionTokenId, positionLiquidity, amount0ToRemove, amount1ToRemove); + + // The withdrawn ETH is transferred to the locker owner, not to the agent triggering the withdrawal + assertGt(ALICE.balance, aliceEthBalanceBefore, "ALICE ETH balance should increase"); + assertEq(AGENT.balance, agentEthBalanceBefore, "AGENT ETH balance should not change"); + assertEq(FluidLocker(payable(address(aliceLocker))).activePositionCount(), 0, "position count should be 0"); + } + + function testAgentCanWithdrawDustETH(uint256 ethAmount) external { + ethAmount = bound(ethAmount, 1, 1_000_000 ether); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + + vm.deal(address(aliceLocker), ethAmount); + + uint256 aliceEthBalanceBefore = ALICE.balance; + + vm.prank(AGENT); + aliceLocker.withdrawDustETH(); + + // The dust ETH is transferred to the locker owner, not to the agent triggering the withdrawal + assertEq(ALICE.balance, aliceEthBalanceBefore + ethAmount, "ALICE ETH balance should increase"); + assertEq(AGENT.balance, 0, "AGENT ETH balance should not change"); + } + + function testNonAgentStillCannotOperate(address caller) external { + vm.assume(caller != ALICE && caller != AGENT && caller != address(0)); + + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + _helperFundLocker(address(aliceLocker), 10_000e18); + + vm.prank(caller); + vm.expectRevert(IFluidLocker.NOT_LOCKER_OWNER.selector); + aliceLocker.stake(10_000e18); + + // The owner is unaffected by the agent being set + vm.prank(ALICE); + aliceLocker.stake(10_000e18); + + assertEq(aliceLocker.getStakedBalance(), 10_000e18, "Staked balance should be 10_000e18"); + } + + function testCannotOperateWhenNoAgentIsSet(address caller) external { + vm.assume(caller != ALICE && caller != address(0)); + + _helperFundLocker(address(aliceLocker), 10_000e18); + + vm.prank(caller); + vm.expectRevert(IFluidLocker.NOT_LOCKER_OWNER.selector); + aliceLocker.stake(10_000e18); + } + + function testAgentOfOneLockerCannotOperateAnotherLocker() external { + _helperSetLockerOwnerAgent(aliceLocker, ALICE, AGENT); + _helperFundLocker(address(bobLocker), 10_000e18); + + // ALICE's agent has no power over BOB's locker + vm.prank(AGENT); + vm.expectRevert(IFluidLocker.NOT_LOCKER_OWNER.selector); + bobLocker.stake(10_000e18); + } +} + +/// @dev Minimal mock allowing the FluidLocker constructor to run with zero-addresses +contract MockStakingRewardController { + function lpDistributionPool() external pure returns (address) { + return address(0); + } + + function taxDistributionPool() external pure returns (address) { + return address(0); + } +} + contract FluidLockerLayoutTest is FluidLocker { constructor() FluidLocker( ISuperToken(address(0)), IEPProgramManager(address(0)), - IStakingRewardController(address(0)), + IStakingRewardController(address(new MockStakingRewardController())), address(0), true, INonfungiblePositionManager(address(0)), @@ -1967,6 +2298,19 @@ contract FluidLockerLayoutTest is FluidLocker { ) { } + function testLockerOwnerAgentStorageLayout() external pure { + uint256 slot; + uint256 offset; + + // V3 storage : lockerOwnerAgent must be appended after the V2 storage + // (V1 : slots 0-2, V2 : slots 3-6) + assembly { + slot := lockerOwnerAgent.slot + offset := lockerOwnerAgent.offset + } + require(slot == 7 && offset == 0, "lockerOwnerAgent changed location"); + } + // function testStorageLayout() external pure { // uint256 slot; // uint256 offset;