diff --git a/foundry/src/FoxStaking.sol b/foundry/src/FoxStakingV1.sol similarity index 97% rename from foundry/src/FoxStaking.sol rename to foundry/src/FoxStakingV1.sol index 9f995f6..45b9a29 100644 --- a/foundry/src/FoxStaking.sol +++ b/foundry/src/FoxStakingV1.sol @@ -9,14 +9,13 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; import {IFoxStaking, StakingInfo} from "./IFoxStaking.sol"; -contract FoxStaking is +contract FoxStakingV1 is Initializable, PausableUpgradeable, UUPSUpgradeable, OwnableUpgradeable { using SafeERC20 for IERC20; - uint256 public version; IERC20 public foxToken; mapping(address => StakingInfo) public stakingInfo; bool public stakingPaused; @@ -47,7 +46,6 @@ contract FoxStaking is __Ownable_init(msg.sender); __UUPSUpgradeable_init(); __Pausable_init(); - version = 1; foxToken = IERC20(foxTokenAddress); stakingPaused = false; withdrawalsPaused = false; @@ -59,6 +57,10 @@ contract FoxStaking is address newImplementation ) internal override onlyOwner {} + function version() external view returns (uint256) { + return _getInitializedVersion(); + } + function pauseStaking() external onlyOwner { stakingPaused = true; } diff --git a/foundry/test/FoxStakingTestUpgrades.t.sol b/foundry/test/FoxStakingTestUpgrades.t.sol index dfb4248..7f53731 100644 --- a/foundry/test/FoxStakingTestUpgrades.t.sol +++ b/foundry/test/FoxStakingTestUpgrades.t.sol @@ -2,29 +2,138 @@ pragma solidity ^0.8.25; import "forge-std/Test.sol"; -import "../src/FoxStaking.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {FoxStakingV1} from "../src/FoxStakingV1.sol"; import {MockFOXToken} from "./MockFOXToken.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {IFoxStaking, StakingInfo} from "../src/IFoxStaking.sol"; -contract FOXStakingTestUpgrades is Test { - FoxStaking public foxStaking; +/// @custom:oz-upgrades-from FoxStakingV1 +contract MockFoxStakingV2 is + Initializable, + PausableUpgradeable, + UUPSUpgradeable, + OwnableUpgradeable +{ + using SafeERC20 for IERC20; + IERC20 public foxToken; + mapping(address => StakingInfo) public stakingInfo; + bool public stakingPaused; + bool public withdrawalsPaused; + bool public unstakingPaused; + uint256 public cooldownPeriod; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize() external reinitializer(2) {} + + function _authorizeUpgrade( + address newImplementation + ) internal override onlyOwner {} + + function version() external view returns (uint256) { + return _getInitializedVersion(); + } + + /// New function in v2 + function newV2Function() public pure returns (string memory) { + return "new v2 function"; + } +} + +contract UpgradeHelper is Test { + /// @dev Wrapper to perform upgrades pranking the owner. Required to make revert reasons + /// consistent - otherwise vm.expectRevert actually reverts with a different reason when present + /// versus when not present. + /// https://github.com/foundry-rs/foundry/issues/5454 + function doUpgrade(address prankOwner, address proxy) public { + vm.startPrank(prankOwner); + Upgrades.upgradeProxy( + proxy, + "FoxStakingTestUpgrades.t.sol:MockFoxStakingV2", + abi.encodeCall(MockFoxStakingV2.initialize, ()) + ); + vm.stopPrank; + } +} + +contract FoxStakingTestUpgrades is Test { + address public owner = address(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045); + address public foxStakingProxy; + FoxStakingV1 public foxStakingV1; MockFOXToken public foxToken; - address user = address(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045); + UpgradeHelper public upgradeHelper; function setUp() public { + upgradeHelper = new UpgradeHelper(); foxToken = new MockFOXToken(); - address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + + vm.startPrank(owner); + foxStakingProxy = Upgrades.deployUUPSProxy( + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + vm.stopPrank(); + + foxStakingV1 = FoxStakingV1(foxStakingProxy); + } + + function testDeployerIsOwner() public view { + assertEq(Ownable(foxStakingProxy).owner(), owner); } - function testCanDeploy() public view { - uint256 expectedVersion = 1; - assertEq(foxStaking.version(), expectedVersion); + function testOwnerCanUpgrade() public { + // Check the current version + uint256 expectedCurrentVersion = 1; + assertEq(foxStakingV1.version(), expectedCurrentVersion); + + // Check we cannot call the new function + vm.expectRevert(); + MockFoxStakingV2 fakeUpgradedFoxStakingV1 = MockFoxStakingV2( + address(foxStakingV1) + ); + fakeUpgradedFoxStakingV1.newV2Function(); + + // Perform the upgrade + upgradeHelper.doUpgrade(owner, foxStakingProxy); + + MockFoxStakingV2 foxStakingV2 = MockFoxStakingV2(foxStakingProxy); + + // Check the new version + uint256 expectedUpgradedVersion = 2; + assertEq(foxStakingV2.version(), expectedUpgradedVersion); + + // Check we can call the new function + string memory result = foxStakingV2.newV2Function(); + assertEq(result, "new v2 function"); + } + + function testNonOwnerCannotUpgrade() public { + // Check the current version + uint256 expectedCurrentVersion = 1; + assertEq(foxStakingV1.version(), expectedCurrentVersion); + + address nonOwner = address(0xBADD1E); + + vm.expectRevert( + abi.encodeWithSelector( + Ownable.OwnableUnauthorizedAccount.selector, + address(nonOwner) + ) + ); + + // Attempt to perform the upgrade, but as a non-owner + upgradeHelper.doUpgrade(nonOwner, foxStakingProxy); } } diff --git a/foundry/test/FoxStaking.t.sol b/foundry/test/FoxStakingV1.t.sol similarity index 95% rename from foundry/test/FoxStaking.t.sol rename to foundry/test/FoxStakingV1.t.sol index 38c121a..540a98a 100644 --- a/foundry/test/FoxStaking.t.sol +++ b/foundry/test/FoxStakingV1.t.sol @@ -2,12 +2,12 @@ pragma solidity ^0.8.25; import "forge-std/Test.sol"; -import "../src/FoxStaking.sol"; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/utils/Strings.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {Pausable} from "@openzeppelin/contracts/utils/Pausable.sol"; import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol"; +import {FoxStakingV1} from "../src/FoxStakingV1.sol"; contract MockFOXToken is ERC20 { constructor() ERC20("Mock FOX Token", "FOX") { @@ -21,17 +21,17 @@ contract MockFOXToken is ERC20 { } contract FOXStakingTestRuneAddress is Test { - FoxStaking public foxStaking; + FoxStakingV1 public foxStaking; MockFOXToken public foxToken; address user = address(0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045); function setUp() public { foxToken = new MockFOXToken(); address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + foxStaking = FoxStakingV1(foxStakingProxy); } function testCanSetRuneAddress() public { @@ -87,17 +87,17 @@ contract FOXStakingTestRuneAddress is Test { } contract FOXStakingTestOwnership is Test { - FoxStaking public foxStaking; + FoxStakingV1 public foxStaking; MockFOXToken public foxToken; address nonOwner = 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045; function setUp() public { foxToken = new MockFOXToken(); address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + foxStaking = FoxStakingV1(foxStakingProxy); } function testOwnerCanUpdateCooldownPeriod() public { @@ -126,16 +126,16 @@ contract FOXStakingTestOwnership is Test { } contract FOXStakingTestStaking is Test { - FoxStaking public foxStaking; + FoxStakingV1 public foxStaking; MockFOXToken public foxToken; function setUp() public { foxToken = new MockFOXToken(); address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + foxStaking = FoxStakingV1(foxStakingProxy); } function testCannotStakeWhenStakingPaused() public { @@ -349,7 +349,7 @@ contract FOXStakingTestStaking is Test { } contract FOXStakingTestUnstake is Test { - FoxStaking public foxStaking; + FoxStakingV1 public foxStaking; MockFOXToken public foxToken; address user = address(0xBEEF); uint256 amount = 1000; @@ -359,10 +359,10 @@ contract FOXStakingTestUnstake is Test { function setUp() public { foxToken = new MockFOXToken(); address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + foxStaking = FoxStakingV1(foxStakingProxy); // Free FOX tokens for user foxToken.makeItRain(user, amount); @@ -634,7 +634,7 @@ contract FOXStakingTestUnstake is Test { } contract FOXStakingTestWithdraw is Test { - FoxStaking public foxStaking; + FoxStakingV1 public foxStaking; MockFOXToken public foxToken; address user = address(0xBEEF); uint256 amount = 1000; @@ -644,10 +644,10 @@ contract FOXStakingTestWithdraw is Test { function setUp() public { foxToken = new MockFOXToken(); address foxStakingProxy = Upgrades.deployUUPSProxy( - "FoxStaking.sol", - abi.encodeCall(FoxStaking.initialize, (address(foxToken))) + "FoxStakingV1.sol", + abi.encodeCall(FoxStakingV1.initialize, (address(foxToken))) ); - foxStaking = FoxStaking(foxStakingProxy); + foxStaking = FoxStakingV1(foxStakingProxy); // Free FOX tokens for user foxToken.makeItRain(user, amount);