diff --git a/src/script/Deploy.s.sol b/src/script/Deploy.s.sol deleted file mode 100644 index 33403a2..0000000 --- a/src/script/Deploy.s.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// slither-disable-start reentrancy-benign - -pragma solidity ^0.8.23; - -import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {Script} from "forge-std/Script.sol"; - -import {DeployInput} from "./DeployInput.sol"; -import {StakerHarness} from "../../test/harnesses/StakerHarness.sol"; -import {IERC20Staking} from "../interfaces/IERC20Staking.sol"; -import {IEarningPowerCalculator} from "../interfaces/IEarningPowerCalculator.sol"; - -contract Deploy is Script, DeployInput { - uint256 deployerPrivateKey; - - function setUp() public { - deployerPrivateKey = vm.envOr( - "DEPLOYER_PRIVATE_KEY", - uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) - ); - } - - function run() public returns (StakerHarness) { - vm.startBroadcast(deployerPrivateKey); - // Deploy the staking contract - StakerHarness govStaker = new StakerHarness( - IERC20(PAYOUT_TOKEN_ADDRESS), - IERC20Staking(STAKE_TOKEN_ADDRESS), - IEarningPowerCalculator(address(0)), - MAX_BUMP_TIP, - vm.addr(deployerPrivateKey), - "StakerHarness" - ); - - // Change Staker admin from `msg.sender` to the Governor timelock - govStaker.setAdmin(GOVERNOR_TIMELOCK); - vm.stopBroadcast(); - - return govStaker; - } -} diff --git a/src/script/DeployBase.sol b/src/script/DeployBase.sol new file mode 100644 index 0000000..2d9858f --- /dev/null +++ b/src/script/DeployBase.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// slither-disable-start reentrancy-benign + +pragma solidity ^0.8.23; + +import {Script} from "forge-std/Script.sol"; +import {IEarningPowerCalculator} from "../interfaces/IEarningPowerCalculator.sol"; +import {Staker} from "../Staker.sol"; + +abstract contract DeployBase is Script { + /// @notice An array of initial reward notifiers for Staker. + address[] internal rewardNotifiers; + /// @notice The address deploying the staking system. + address deployer; + + /// @notice The configuration needed for this base script. + /// @param admin The final admin of the staker contract. + struct BaseConfiguration { + address admin; + } + + /// @notice An interface method that returns a set configuration for the base script. + /// @return The base configuration for the staking system. + function _baseConfiguration() internal virtual returns (BaseConfiguration memory); + + /// @notice An interface method that deploys the Staker contract for the staking system. + /// @param _earningPowerCalculator The address of the deployed earning power calculator. + /// @return The Staker contract for the staking system. + function _deployStaker(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + returns (Staker); + + /// @notice An interface method that deploys the earning power contract for the staking system. + /// @return The earning power calculator contract. + function _deployEarningPowerCalculator() internal virtual returns (IEarningPowerCalculator); + + /// @notice An interface method that deploys the reward notifiers. + /// @param _staker The Staker for the staking system. + function _deployRewardNotifiers(Staker _staker) internal virtual; + + /// @notice The method that is executed when the script runs which deploys the entire staking + /// system. + /// @return The Staker contract, earning power calculator, and array of reward notifiers. + function run() public returns (IEarningPowerCalculator, Staker, address[] memory) { + uint256 deployerPrivateKey = vm.envOr( + "DEPLOYER_PRIVATE_KEY", + uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80) + ); + + deployer = vm.rememberKey(deployerPrivateKey); + vm.startBroadcast(deployer); + IEarningPowerCalculator _earningPowerCalculator = _deployEarningPowerCalculator(); + Staker _staker = _deployStaker(_earningPowerCalculator); + + _deployRewardNotifiers(_staker); + for (uint256 i = 0; i < rewardNotifiers.length; i++) { + _staker.setRewardNotifier(rewardNotifiers[i], true); + } + + BaseConfiguration memory _baseConfig = _baseConfiguration(); + _staker.setAdmin(_baseConfig.admin); + vm.stopBroadcast(); + return (_earningPowerCalculator, _staker, rewardNotifiers); + } +} diff --git a/src/script/DeployInput.sol b/src/script/DeployInput.sol deleted file mode 100644 index 61addff..0000000 --- a/src/script/DeployInput.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-only -// slither-disable-start reentrancy-benign - -pragma solidity ^0.8.23; - -contract DeployInput { - address constant GOVERNOR_TIMELOCK = 0x1a9C8182C09F50C8318d769245beA52c32BE35BC; - address constant PAYOUT_TOKEN_ADDRESS = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // WETH - uint256 constant PAYOUT_AMOUNT = 10e18; // 10 (WETH) - address constant STAKE_TOKEN_ADDRESS = 0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984; // UNI - uint256 constant MAX_BUMP_TIP = 100_000e18; // TODO this should be updated before deployment -} diff --git a/src/script/DeployStaker.sol b/src/script/DeployStaker.sol new file mode 100644 index 0000000..85d8f11 --- /dev/null +++ b/src/script/DeployStaker.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {DeployBase} from "./DeployBase.sol"; +import {IEarningPowerCalculator} from "../interfaces/IEarningPowerCalculator.sol"; +import {Staker} from "../Staker.sol"; + +abstract contract DeployStaker is DeployBase { + /// @notice The configuration for the Staker contract. + /// @param rewardToken The reward token for Staker. + /// @param stakeToken The stake token for Staker. + /// @param earningPowerCalculator The earning power calculator for Staker. + /// @param maxBumpTip The max bump tip for Staker. + struct StakerConfiguration { + IERC20 rewardToken; + IERC20 stakeToken; + IEarningPowerCalculator earningPowerCalculator; + uint256 maxBumpTip; + } + + /// @notice An interface method that returns a the configuration for the Staker contract. + /// @param _earningPowerCalculator The deployed earning power calculator. + /// @return The staker configration. + function _stakerConfiguration(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + returns (StakerConfiguration memory); +} diff --git a/src/script/calculators/DeployIdentityEarningPowerCalculator.sol b/src/script/calculators/DeployIdentityEarningPowerCalculator.sol new file mode 100644 index 0000000..40ac89e --- /dev/null +++ b/src/script/calculators/DeployIdentityEarningPowerCalculator.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {DeployBase} from "../DeployBase.sol"; +import {IdentityEarningPowerCalculator} from "../../calculators/IdentityEarningPowerCalculator.sol"; +import {IEarningPowerCalculator} from "../../interfaces/IEarningPowerCalculator.sol"; + +abstract contract DeployIdentityEarningPowerCalculator is DeployBase { + /// @notice Deploys an identitiy earning power calculator. + /// @inheritdoc DeployBase + function _deployEarningPowerCalculator() + internal + virtual + override + returns (IEarningPowerCalculator) + { + return new IdentityEarningPowerCalculator(); + } +} diff --git a/src/script/notifiers/DeployMinterRewardNotifier.sol b/src/script/notifiers/DeployMinterRewardNotifier.sol new file mode 100644 index 0000000..d7d70b1 --- /dev/null +++ b/src/script/notifiers/DeployMinterRewardNotifier.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {INotifiableRewardReceiver} from "../../interfaces/INotifiableRewardReceiver.sol"; +import {IMintable} from "../../interfaces/IMintable.sol"; +import {DeployBase} from "../DeployBase.sol"; +import {MintRewardNotifier} from "../../notifiers/MintRewardNotifier.sol"; +import {Staker} from "../../Staker.sol"; + +abstract contract DeployMinterRewardNotifier is DeployBase { + /// @notice The configuration for the minter reward notifier. + /// @param _receiver The contract that will receive reward notifications, typically an instance + /// of Staker. + /// @param _initialRewardAmount The initial amount of reward tokens to be distributed per + /// notification. + /// @param _initialRewardInterval The initial minimum time that must elapse between + /// notifications. + /// @param _initialOwner The address that will have permission to update contract parameters. + /// @param _minter The initial contract authorized to mint reward tokens. + struct MinterRewardNotifierConfiguration { + uint256 initialRewardAmount; + uint256 initialRewardInterval; + address initialOwner; + IMintable minter; + } + + /// @notice An interface method that returns the configuration for the minter reward notifier. + function _minterRewardNotifierConfiguration() + internal + virtual + returns (MinterRewardNotifierConfiguration memory); + + /// @notice Deploys a minter reward notifier. + /// @inheritdoc DeployBase + /// @dev When this method is overridden make sure to call super so it is added to the reward + /// notifiers array. + function _deployRewardNotifiers(Staker _staker) internal virtual override { + MinterRewardNotifierConfiguration memory _config = _minterRewardNotifierConfiguration(); + MintRewardNotifier _notifier = new MintRewardNotifier( + INotifiableRewardReceiver(address(_staker)), + _config.initialRewardAmount, + _config.initialRewardInterval, + _config.initialOwner, + _config.minter + ); + rewardNotifiers.push(address(_notifier)); + } +} diff --git a/src/script/notifiers/DeployTransferFromRewardNotifier.sol b/src/script/notifiers/DeployTransferFromRewardNotifier.sol new file mode 100644 index 0000000..c9cac0a --- /dev/null +++ b/src/script/notifiers/DeployTransferFromRewardNotifier.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {INotifiableRewardReceiver, IERC20} from "../../interfaces/INotifiableRewardReceiver.sol"; +import {TransferFromRewardNotifier} from "../../notifiers/TransferFromRewardNotifier.sol"; +import {DeployBase} from "../DeployBase.sol"; +import {Staker} from "../../Staker.sol"; + +abstract contract DeployTransferFromRewardNotifier is DeployBase { + /// @notice The configuration for the transferFrom reward notifier. + /// @param initialRewardAmount The initial amount of reward tokens to be distributed per + /// notification. + /// @param initialRewardInterval The initial minimum time that must elapse between + /// notifications. + /// @param initialOwner The address that will have permission to update contract parameters. + /// @param initialRewardSource The initial source of reward tokens. + struct TransferFromRewardNotifierConfiguration { + uint256 initialRewardAmount; + uint256 initialRewardInterval; + address initialOwner; + address initialRewardSource; + } + + /// @notice An interface method that returns the configuration for the transferFrom reward + /// notifier. + function _transferFromRewardNotifierConfiguration() + internal + virtual + returns (TransferFromRewardNotifierConfiguration memory); + + /// @notice Deploys a transferFrom reward notifier. + /// @inheritdoc DeployBase + /// @dev When this method is overridden make sure to call super so it is added to the reward + /// notifiers array. + function _deployRewardNotifiers(Staker _staker) internal virtual override { + TransferFromRewardNotifierConfiguration memory _config = + _transferFromRewardNotifierConfiguration(); + TransferFromRewardNotifier _notifier = new TransferFromRewardNotifier( + INotifiableRewardReceiver(address(_staker)), + _config.initialRewardAmount, + _config.initialRewardInterval, + _config.initialOwner, + _config.initialRewardSource + ); + rewardNotifiers.push(address(_notifier)); + } +} diff --git a/test/DeployBase.t.sol b/test/DeployBase.t.sol new file mode 100644 index 0000000..62e5fe1 --- /dev/null +++ b/test/DeployBase.t.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {IEarningPowerCalculator} from "../src/interfaces/IEarningPowerCalculator.sol"; +import {MintRewardNotifier} from "../src/notifiers/MintRewardNotifier.sol"; +import {Staker} from "../src/Staker.sol"; +import {DeployBaseFake} from "./fakes/DeployBaseFake.sol"; +import {ERC20Fake} from "./fakes/ERC20Fake.sol"; +import {ERC20VotesMock} from "./mocks/MockERC20Votes.sol"; + +contract DeployBaseTest is Test { + ERC20Fake rewardToken; + ERC20VotesMock govToken; + DeployBaseFake deployScript; + + function setUp() public { + rewardToken = new ERC20Fake(); + vm.label(address(rewardToken), "Reward Token"); + + govToken = new ERC20VotesMock(); + vm.label(address(govToken), "Governance Token"); + + deployScript = new DeployBaseFake(rewardToken, govToken); + } +} + +contract Run is DeployBaseTest { + function test_StakingSystemDeploy() public { + (IEarningPowerCalculator _calculator, Staker _staker, address[] memory _notifiers) = + deployScript.run(); + MintRewardNotifier _mintNotifier = MintRewardNotifier(_notifiers[0]); + assertEq(address(_staker), address(_mintNotifier.RECEIVER())); + assertEq(10e18, _mintNotifier.rewardAmount()); + assertEq(30 days, _mintNotifier.rewardInterval()); + assertEq(deployScript.notifierOwner(), _mintNotifier.owner()); + assertEq(address(deployScript.notifierMinter()), address(_mintNotifier.minter())); + + // Staker params + assertTrue(_staker.isRewardNotifier(_notifiers[0])); + assertEq(address(_calculator), address(_staker.earningPowerCalculator())); + assertEq(address(rewardToken), address(_staker.REWARD_TOKEN())); + assertEq(address(govToken), address(_staker.STAKE_TOKEN())); + assertEq(address(deployScript.admin()), _staker.admin()); + } +} diff --git a/test/DeployTransferFromRewardNotifier.t.sol b/test/DeployTransferFromRewardNotifier.t.sol new file mode 100644 index 0000000..2db867e --- /dev/null +++ b/test/DeployTransferFromRewardNotifier.t.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {Test} from "forge-std/Test.sol"; +import {Staker} from "../src/Staker.sol"; +import {TransferFromRewardNotifier} from "../src/notifiers/TransferFromRewardNotifier.sol"; +import {DeployTransferFromRewardNotifierFake} from + "./fakes/DeployTransferFromRewardNotifierFake.sol"; +import {ERC20Fake} from "./fakes/ERC20Fake.sol"; +import {ERC20VotesMock} from "./mocks/MockERC20Votes.sol"; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; + +contract DeployTransferFromRewardNotifierTest is Test { + ERC20Fake rewardToken; + ERC20VotesMock govToken; + DeployTransferFromRewardNotifierFake deployScript; + + function setUp() public { + rewardToken = new ERC20Fake(); + vm.label(address(rewardToken), "Reward Token"); + + govToken = new ERC20VotesMock(); + vm.label(address(govToken), "Governance Token"); + + deployScript = new DeployTransferFromRewardNotifierFake(rewardToken, govToken); + } +} + +contract run is DeployTransferFromRewardNotifierTest { + function test_DeployTransferFromRewardNotifier() public { + (, Staker _staker, address[] memory _notifiers) = deployScript.run(); + + TransferFromRewardNotifier _transferFromNotifier = TransferFromRewardNotifier(_notifiers[0]); + assertEq(address(_staker), address(_transferFromNotifier.RECEIVER())); + assertEq(10e18, _transferFromNotifier.rewardAmount()); + assertEq(30 days, _transferFromNotifier.rewardInterval()); + assertEq(deployScript.notifierOwner(), _transferFromNotifier.owner()); + assertEq( + address(deployScript.notifierRewardSource()), address(_transferFromNotifier.rewardSource()) + ); + } +} + +contract SetRewardSource is DeployTransferFromRewardNotifierTest { + function testFuzz_UpdatesTheRewardSource(address _newRewardSource) public { + (,, address[] memory _notifiers) = deployScript.run(); + TransferFromRewardNotifier _transferFromNotifier = TransferFromRewardNotifier(_notifiers[0]); + address owner = deployScript.notifierOwner(); + + vm.prank(owner); + _transferFromNotifier.setRewardSource(_newRewardSource); + + assertEq(_transferFromNotifier.rewardSource(), _newRewardSource); + } + + function testFuzz_EmitsAnEventForSettingTheRewardSource(address _newRewardSource) public { + (,, address[] memory _notifiers) = deployScript.run(); + TransferFromRewardNotifier _transferFromNotifier = TransferFromRewardNotifier(_notifiers[0]); + address owner = deployScript.notifierOwner(); + address _oldRewardSource = deployScript.notifierRewardSource(); + + vm.expectEmit(); + emit TransferFromRewardNotifier.RewardSourceSet(_oldRewardSource, _newRewardSource); + vm.prank(owner); + _transferFromNotifier.setRewardSource(_newRewardSource); + } + + function testFuzz_RevertIf_CallerIsNotOwner(address _newRewardSource, address _notOwner) public { + (,, address[] memory _notifiers) = deployScript.run(); + TransferFromRewardNotifier _transferFromNotifier = TransferFromRewardNotifier(_notifiers[0]); + address owner = deployScript.notifierOwner(); + + vm.assume(_notOwner != owner); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, _notOwner)); + vm.prank(_notOwner); + _transferFromNotifier.setRewardSource(_newRewardSource); + } +} diff --git a/test/fakes/DeployBaseFake.sol b/test/fakes/DeployBaseFake.sol new file mode 100644 index 0000000..722c7b3 --- /dev/null +++ b/test/fakes/DeployBaseFake.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {DeployBase} from "../../src/script/DeployBase.sol"; +import {DeployStaker} from "../../src/script/DeployStaker.sol"; +import {DeployMinterRewardNotifier} from "../../src/script/notifiers/DeployMinterRewardNotifier.sol"; +import {DeployIdentityEarningPowerCalculator} from + "../../src/script/calculators/DeployIdentityEarningPowerCalculator.sol"; +import {IMintable} from "../../src/interfaces/IMintable.sol"; +import {INotifiableRewardReceiver} from "../../src/interfaces/INotifiableRewardReceiver.sol"; + +import {IEarningPowerCalculator} from "../../src/interfaces/IEarningPowerCalculator.sol"; +import {Staker} from "../../src/Staker.sol"; +import {StakerHarness} from "../harnesses/StakerHarness.sol"; +import {IERC20Staking} from "../../src/interfaces/IERC20Staking.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeployBaseFake is + DeployBase, + DeployStaker, + DeployMinterRewardNotifier, + DeployIdentityEarningPowerCalculator +{ + address public admin = makeAddr("Staker admin"); + address public notifierReceiver = makeAddr("Notifier receiver"); + address public notifierOwner = makeAddr("Notifier owner"); + address public notifierMinter = makeAddr("Notifier minter"); + IERC20 public rewardToken; + IERC20 public stakeToken; + + constructor(IERC20 _rewardToken, IERC20 _stakeToken) { + rewardToken = _rewardToken; + stakeToken = _stakeToken; + } + + function _baseConfiguration() internal virtual override returns (BaseConfiguration memory) { + return BaseConfiguration({admin: admin}); + } + + function _minterRewardNotifierConfiguration() + internal + virtual + override + returns (MinterRewardNotifierConfiguration memory) + { + return MinterRewardNotifierConfiguration({ + initialRewardAmount: 10e18, + initialRewardInterval: 30 days, + initialOwner: notifierOwner, + minter: IMintable(notifierMinter) + }); + } + + function _stakerConfiguration(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + override + returns (StakerConfiguration memory) + { + return StakerConfiguration({ + rewardToken: rewardToken, + stakeToken: stakeToken, + earningPowerCalculator: _earningPowerCalculator, + maxBumpTip: 1e18 + }); + } + + function _deployStaker(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + override + returns (Staker) + { + StakerConfiguration memory _config = _stakerConfiguration(_earningPowerCalculator); + return new StakerHarness( + _config.rewardToken, + IERC20Staking(address(_config.stakeToken)), + _config.earningPowerCalculator, + _config.maxBumpTip, + deployer, + "Harness" + ); + } +} diff --git a/test/fakes/DeployTransferFromRewardNotifierFake.sol b/test/fakes/DeployTransferFromRewardNotifierFake.sol new file mode 100644 index 0000000..62afad8 --- /dev/null +++ b/test/fakes/DeployTransferFromRewardNotifierFake.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: AGPL-3.0-only +pragma solidity ^0.8.23; + +import {DeployBase} from "../../src/script/DeployBase.sol"; +import {DeployStaker} from "../../src/script/DeployStaker.sol"; +import {DeployTransferFromRewardNotifier} from + "../../src/script/notifiers/DeployTransferFromRewardNotifier.sol"; +import {DeployIdentityEarningPowerCalculator} from + "../../src/script/calculators/DeployIdentityEarningPowerCalculator.sol"; +import {IEarningPowerCalculator} from "../../src/interfaces/IEarningPowerCalculator.sol"; +import {Staker} from "../../src/Staker.sol"; +import {StakerHarness} from "../harnesses/StakerHarness.sol"; +import {IERC20Staking} from "../../src/interfaces/IERC20Staking.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +contract DeployTransferFromRewardNotifierFake is + DeployBase, + DeployStaker, + DeployTransferFromRewardNotifier, + DeployIdentityEarningPowerCalculator +{ + address public admin = makeAddr("Staker admin"); + address public notifierOwner = makeAddr("Notifier owner"); + address public notifierRewardSource = makeAddr("Notifier reward source"); + + IERC20 rewardToken; + IERC20 stakeToken; + + constructor(IERC20 _rewardToken, IERC20 _stakeToken) { + rewardToken = _rewardToken; + stakeToken = _stakeToken; + } + + function _baseConfiguration() internal virtual override returns (BaseConfiguration memory) { + return BaseConfiguration({admin: admin}); + } + + function _transferFromRewardNotifierConfiguration() + internal + virtual + override + returns (TransferFromRewardNotifierConfiguration memory) + { + return TransferFromRewardNotifierConfiguration({ + initialRewardAmount: 10e18, + initialRewardInterval: 30 days, + initialOwner: notifierOwner, + initialRewardSource: notifierRewardSource + }); + } + + function _stakerConfiguration(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + override + returns (StakerConfiguration memory) + { + return StakerConfiguration({ + rewardToken: rewardToken, + stakeToken: stakeToken, + earningPowerCalculator: _earningPowerCalculator, + maxBumpTip: 1e18 + }); + } + + function _deployStaker(IEarningPowerCalculator _earningPowerCalculator) + internal + virtual + override + returns (Staker) + { + StakerConfiguration memory _config = _stakerConfiguration(_earningPowerCalculator); + return new StakerHarness( + _config.rewardToken, + IERC20Staking(address(_config.stakeToken)), + _config.earningPowerCalculator, + _config.maxBumpTip, + deployer, + "Harness" + ); + } +}