diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0085342 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Deployer private key +PRIVATE_KEY= + +# RPCs +ARBITRUM_RPC_URL= +BASE_RPC_URL= +ETHEREUM_RPC_URL= +FRAXTAL_RPC_URL= +GNOSIS_RPC_URL= +OPTIMISM_RPC_URL= +POLYGON_RPC_URL= + +# Etherscan APIs +ETHERSCAN_ARBITRUM_API_KEY= +ETHERSCAN_BASE_API_KEY= +ETHERSCAN_ETHEREUM_API_KEY= +ETHERSCAN_FRAXTAL_API_KEY= +ETHERSCAN_GNOSIS_API_KEY= +ETHERSCAN_OPTIMISM_API_KEY= +ETHERSCAN_POLYGON_API_KEY= diff --git a/.gitignore b/.gitignore index 4ba749a..11a59c9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ @@ -145,3 +144,20 @@ node_modules # Hardhat Ignition default folder for deployments against a local node ignition/deployments/chain-31337 + +# Foundry +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/**/dry-run/ + +# Codecov +lcov.info + +# Testing +.gas-snapshot + +# vscode - audit docs +.vscode/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f67502a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std.git diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6d08f85 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +-include .env + +# command: test the whole script without broadcasting the transaction into the chain to spot early errors +deployPolygonDry: + forge script foundry_scripts/InjectorInfraDeployment.s.sol \ + --rpc-url polygon \ + --slow \ + -vvvv + +deployPolygonBroadcastAndVerify: + forge script foundry_scripts/InjectorInfraDeployment.s.sol \ + --rpc-url polygon \ + --slow \ + --broadcast \ + --verify \ + -vvvv + +deployMultiChainDry: + forge script foundry_scripts/InjectorInfraMultiChainDeployment.s.sol \ + --rpc-url polygon \ + --slow \ + -- multi \ + -vvvv + +deployMultiChainBroadcastAndVerify: + forge script foundry_scripts/InjectorInfraMultiChainDeployment.s.sol \ + --rpc-url polygon \ + --slow \ + -- multi \ + --broadcast \ + --verify \ + -vvvv + + \ No newline at end of file diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..78e643e --- /dev/null +++ b/foundry.toml @@ -0,0 +1,18 @@ +[profile.default] +src = 'contracts' +out = "out" +libs = ['node_modules','lib'] +test = 'foundry_test' +solc = '0.8.25' + +[fmt] +ignore = ['./contracts/**/*'] + +[rpc_endpoints] +arbitrum = "${ARBITRUM_RPC_URL}" +base = "${BASE_RPC_URL}" +ethereum = "${ETHEREUM_RPC_URL}" +fraxtal = "${FRAXTAL_RPC_URL}" +gnosis = "${GNOSIS_RPC_URL}" +optimism = "${OPTIMISM_RPC_URL}" +polygon = "${POLYGON_RPC_URL}" \ No newline at end of file diff --git a/foundry_scripts/InjectorInfraDeployment.s.sol b/foundry_scripts/InjectorInfraDeployment.s.sol new file mode 100644 index 0000000..ce02a03 --- /dev/null +++ b/foundry_scripts/InjectorInfraDeployment.s.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; + +import {ChildChainGaugeInjectorV2} from "../contracts/ChildChainGaugeInjectorV2.sol"; +import {ChildChainGaugeInjectorV2Factory} from "../contracts/injectorFactoryV2.sol"; + +/// @notice Deploys the v2 infrastructure for the injectors in the following order: +/// 1. {ChildChainGaugeInjectorV2} -> singleton/implementation purposes (helps verifying in etherscan etc) +/// 2. {ChildChainGaugeInjectorV2Factory} +contract InjectorInfraDeployment is Script { + // injector infrastructure + ChildChainGaugeInjectorV2 injectorImpl; + ChildChainGaugeInjectorV2Factory injectorFactory; + + function run() public { + // read pk from `.env` + uint256 pk = vm.envUint("PRIVATE_KEY"); + vm.startBroadcast(pk); + + // 1. {ChildChainGaugeInjectorV2} + injectorImpl = new ChildChainGaugeInjectorV2(); + + // 2. {ChildChainGaugeInjectorV2Factory} + injectorFactory = new ChildChainGaugeInjectorV2Factory(address(injectorImpl)); + } +} \ No newline at end of file diff --git a/foundry_scripts/InjectorInfraMultiChainDeployment.s.sol b/foundry_scripts/InjectorInfraMultiChainDeployment.s.sol new file mode 100644 index 0000000..79bdae7 --- /dev/null +++ b/foundry_scripts/InjectorInfraMultiChainDeployment.s.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.25; + +import {Script} from "forge-std/Script.sol"; + +import {ChildChainGaugeInjectorV2} from "../contracts/ChildChainGaugeInjectorV2.sol"; +import {ChildChainGaugeInjectorV2Factory} from "../contracts/injectorFactoryV2.sol"; + +/// @notice Deploys the v2 infrastructure for the injectors in all chains in `foundry.toml` in the following order: +/// 1. {ChildChainGaugeInjectorV2} -> singleton/implementation purposes (helps verifying in etherscan etc) +/// 2. {ChildChainGaugeInjectorV2Factory} +contract InjectorInfraMultiChainDeployment is Script { + enum Chains { + ARBITRUM, + BASE, + ETHEREUM, + FRAXTAL, + GNOSIS, + OPTIMISM, + POLYGON + } + + // injector infrastructure + ChildChainGaugeInjectorV2 injectorImpl; + ChildChainGaugeInjectorV2Factory injectorFactory; + + mapping(Chains chain => string rpcAlias) public availableChains; + + constructor() { + availableChains[Chains.ARBITRUM] = "arbitrum"; + availableChains[Chains.BASE] = "base"; + availableChains[Chains.ETHEREUM] = "ethereum"; + availableChains[Chains.FRAXTAL] = "fraxtal"; + availableChains[Chains.GNOSIS] = "gnosis"; + availableChains[Chains.OPTIMISM] = "optimism"; + availableChains[Chains.POLYGON] = "polygon"; + } + + /// @dev broadcast transaction modifier + /// @param pk private key to broadcast transaction + modifier broadcast(uint256 pk) { + vm.startBroadcast(pk); + + _; + + vm.stopBroadcast(); + } + + function run() public { + // read pk from `.env` + uint256 pk = vm.envUint("PRIVATE_KEY"); + + // @note the array can be updated depending on your target chains to deploy + // @note by default the script will deploy in all chains available in the toml file + Chains[] memory targetDeploymentChains = new Chains[](6); + + targetDeploymentChains[0] = Chains.ARBITRUM; + targetDeploymentChains[1] = Chains.BASE; + targetDeploymentChains[2] = Chains.ETHEREUM; + targetDeploymentChains[3] = Chains.GNOSIS; + targetDeploymentChains[4] = Chains.OPTIMISM; + targetDeploymentChains[5] = Chains.POLYGON; + // @note fraxtal rpc gives sometimes problems + + for (uint256 i = 0; i < targetDeploymentChains.length; i++) { + _deploy(targetDeploymentChains[i], pk); + } + } + + /// @dev Helper to point into a specific chain + /// @param _targetChain chain to deploy + /// @param _pk private key to broadcast transaction + function _deploy(Chains _targetChain, uint256 _pk) internal { + vm.createSelectFork(availableChains[_targetChain]); + + _infraDeployment(_pk); + } + + /// @dev Helper to deploy the factory and singleton + /// @param _pk private key to broadcast transaction + function _infraDeployment(uint256 _pk) internal broadcast(_pk) { + // 1. {ChildChainGaugeInjectorV2} + injectorImpl = new ChildChainGaugeInjectorV2(); + + // 2. {ChildChainGaugeInjectorV2Factory} + injectorFactory = new ChildChainGaugeInjectorV2Factory(address(injectorImpl)); + } +} \ No newline at end of file diff --git a/foundry_test/BaseFixture.sol b/foundry_test/BaseFixture.sol new file mode 100644 index 0000000..cec942f --- /dev/null +++ b/foundry_test/BaseFixture.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import "forge-std/Test.sol"; + +import {IChildChainGauge} from "../contracts/interfaces/balancer/IChildChainGauge.sol"; + +import {ChildChainGaugeInjectorV2} from "../contracts/ChildChainGaugeInjectorV2.sol"; +import {ChildChainGaugeInjectorV2Factory} from "../contracts/injectorFactoryV2.sol"; + +contract BaseFixture is Test { + // injector instance + ChildChainGaugeInjectorV2 injector; + + // factory instance + ChildChainGaugeInjectorV2Factory factory; + + // constants + address constant GAUGE = 0x3Eae4a1c2E36870A006E816930d9f55DF0a72a13; + address constant GAUGE_2 = 0xc7e5FE004416A96Cb2C7D6440c28aE92262f7695; + address constant LM_MULTISIG = 0xc38c5f97B34E175FFd35407fc91a937300E33860; + address constant AUTHORIZER_ADAPTER = 0xAB093cd16e765b5B23D34030aaFaF026558e0A19; + address constant TEST_TOKEN_WHALE = 0xF977814e90dA44bFA03b6295A0616a897441aceC; + + // token address constants + address constant USDT = 0xc2132D05D31c914a87C6611C10748AEb04B58e8F; + address constant USDC = 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174; + + // agents + address constant KEEPER = address(5); + + address[] KEEPER_ADDRESSES = new address[](1); + + // dummy constants + uint256 MIN_WAIT_PERIOD_SECONDS = 1 days; + uint256 MAX_INJECTION_AMOUNT = 1_000e18; + address OWNER = address(56565); + + event InjectorCreated( + address indexed injector, address[] keeperAddresses, address injectTokenAddress, address owner + ); + + function setUp() public { + vm.createSelectFork("polygon"); + + injector = new ChildChainGaugeInjectorV2(); + factory = new ChildChainGaugeInjectorV2Factory(address(injector)); + + assert(factory.implementation() == address(injector)); + } + + function _deployDummyInjector() internal returns (address injectorDeployed_) { + KEEPER_ADDRESSES[0] = KEEPER; + + // check: event emitted + vm.expectEmit(false, true, true, true); // @note topic0 is not checkeds + emit InjectorCreated(address(0), KEEPER_ADDRESSES, USDT, OWNER); + + injectorDeployed_ = + factory.createInjector(KEEPER_ADDRESSES, MIN_WAIT_PERIOD_SECONDS, USDT, MAX_INJECTION_AMOUNT, OWNER); + } + + function _enableInjectorAsDistributor(address _injector) internal { + IChildChainGauge gaugeFirst = IChildChainGauge(GAUGE); + IChildChainGauge gaugeSecond = IChildChainGauge(GAUGE_2); + + vm.prank(gaugeFirst.authorizer_adaptor()); + gaugeFirst.add_reward(USDT, _injector); + vm.prank(gaugeSecond.authorizer_adaptor()); + gaugeSecond.add_reward(USDT, _injector); + } +} diff --git a/foundry_test/FactoryTest.t.sol b/foundry_test/FactoryTest.t.sol new file mode 100644 index 0000000..8afb756 --- /dev/null +++ b/foundry_test/FactoryTest.t.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {BaseFixture} from "./BaseFixture.sol"; + +import {ChildChainGaugeInjectorV2} from "../contracts/ChildChainGaugeInjectorV2.sol"; + +contract FactoryTest is BaseFixture { + function testCreateInjector() public { + // 1. create a new injector via factory + address injectorDeployed = _deployDummyInjector(); + ChildChainGaugeInjectorV2 injectorFactoryDeployed = ChildChainGaugeInjectorV2(injectorDeployed); + + // 2. asserts: + // 2.1. check `getDeployedInjectors` returns the correct number of injectors + address[] memory injectorsDeployed = factory.getDeployedInjectors(); + assertEq(injectorsDeployed.length, 1); + assertEq(injectorsDeployed[0], injectorDeployed); + + // 2.2. check params of the injector correctness at deployment time + assertEq(injectorFactoryDeployed.owner(), OWNER); + assertEq(injectorFactoryDeployed.getKeeperAddresses()[0], KEEPER); + assertEq(injectorFactoryDeployed.MinWaitPeriodSeconds(), MIN_WAIT_PERIOD_SECONDS); + assertEq(injectorFactoryDeployed.InjectTokenAddress(), USDT); + } +} diff --git a/foundry_test/UncoveredLinesTest.t.sol b/foundry_test/UncoveredLinesTest.t.sol new file mode 100644 index 0000000..30b7268 --- /dev/null +++ b/foundry_test/UncoveredLinesTest.t.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.25; + +import {BaseFixture} from "./BaseFixture.sol"; + +import "../node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +import {IChildChainGauge} from "../contracts/interfaces/balancer/IChildChainGauge.sol"; + +import {ChildChainGaugeInjectorV2} from "../contracts/ChildChainGaugeInjectorV2.sol"; + +/// @notice Scope of the file is to test uncovered reverts, setters and getters: +/// 1. revert: `InjectorNotDistributor` +/// 2. revert: `ExceedsTotalInjectorProgramBudget` +/// 3. revert: `OnlyKeepers` +/// 4. getter: `getBalanceDelta` (cases-> deficit, exact balance and surplus) +/// 5. getter: `getFullSchedule` (check: expected values) +// @audit https://github.com/BalancerMaxis/ChildGaugeInjectorV2/issues/31 ? +contract UncoveredLinesTest is BaseFixture { + function test_revertWhen_InjectorNotDistributor() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + address[] memory recipients = new address[](1); + recipients[0] = GAUGE; + + vm.prank(inj.owner()); + vm.expectRevert(abi.encodeWithSelector(ChildChainGaugeInjectorV2.InjectorNotDistributor.selector, GAUGE, USDT)); + inj.addRecipients(recipients, 50e18, 4, uint56(block.timestamp + 1 days)); + } + + function test_revertWhen_ExceedsTotalInjectorProgramBudget() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + _enableInjectorAsDistributor(address(inj)); + + uint256 dummyMaxTotalDue = 100e18; + vm.startPrank(inj.owner()); + inj.setMaxTotalDue(dummyMaxTotalDue); + assertEq(inj.MaxTotalDue(), dummyMaxTotalDue); + + address[] memory recipients = new address[](2); + recipients[0] = GAUGE; + recipients[1] = GAUGE_2; + + uint256 amountPerPeriod = 250e18; + vm.expectRevert( + abi.encodeWithSelector( + ChildChainGaugeInjectorV2.ExceedsTotalInjectorProgramBudget.selector, amountPerPeriod + ) + ); + inj.addRecipients(recipients, 250e18, 1, uint56(block.timestamp + 1 days)); + } + + function test_revertWhen_NotKeepers() public { + address NOT_KEEPER_AGENT = address(543485484845); + + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + address[] memory needsFunding = new address[](1); + needsFunding[0] = GAUGE; + + vm.prank(NOT_KEEPER_AGENT); + vm.expectRevert(abi.encodeWithSelector(ChildChainGaugeInjectorV2.OnlyKeepers.selector, NOT_KEEPER_AGENT)); + inj.performUpkeep(abi.encode(needsFunding)); + } + + function testGetBalance_When_Deficit() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + _enableInjectorAsDistributor(address(inj)); + + address[] memory recipients = new address[](1); + recipients[0] = GAUGE; + uint256 amountPerPeriod = 250e18; + vm.prank(inj.owner()); + inj.addRecipients(recipients, amountPerPeriod, 1, uint56(block.timestamp + 1 days)); + + // send partially + deal(USDT, address(inj), 50e18); + + int256 expectedDeficit = -1 * int256(amountPerPeriod - IERC20(USDT).balanceOf(address(inj))); + + // should encounter DEFICIT + assertEq(inj.getBalanceDelta(), expectedDeficit); + } + + function testGetBalance_When_ExactBalance() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + _enableInjectorAsDistributor(address(inj)); + + address[] memory recipients = new address[](1); + recipients[0] = GAUGE; + uint256 amountPerPeriod = 250e18; + vm.prank(inj.owner()); + inj.addRecipients(recipients, amountPerPeriod, 1, uint56(block.timestamp + 1 days)); + + // send full `amountPerPeriod` + deal(USDT, address(inj), amountPerPeriod); + + // should encounter EXACT + assertEq(inj.getBalanceDelta(), 0); + } + + function testGetBalance_When_Surplus() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + _enableInjectorAsDistributor(address(inj)); + + address[] memory recipients = new address[](1); + recipients[0] = GAUGE; + uint256 amountPerPeriod = 250e18; + vm.prank(inj.owner()); + inj.addRecipients(recipients, amountPerPeriod, 1, uint56(block.timestamp + 1 days)); + + // send full `amountPerPeriod` * 3 + deal(USDT, address(inj), amountPerPeriod * 3); + + int256 expectedSurplus = int256(IERC20(USDT).balanceOf(address(inj)) - amountPerPeriod); + // should encounter SURPLUS + assertEq(inj.getBalanceDelta(), expectedSurplus); + } + + function testGetFullSchedule() public { + ChildChainGaugeInjectorV2 inj = ChildChainGaugeInjectorV2(_deployDummyInjector()); + + _enableInjectorAsDistributor(address(inj)); + + address[] memory recipients = new address[](2); + recipients[0] = GAUGE; + recipients[1] = GAUGE_2; + + vm.prank(inj.owner()); + inj.addRecipients(recipients, 250e18, 5, uint56(block.timestamp + 1 days)); + + ( + address[] memory gauges, + uint256[] memory amountsPerPeriod, + uint8[] memory maxPeriods, + uint8[] memory currentPeriods, + uint56[] memory lastTimestamps, + uint56[] memory doNotStartBeforeTimestamps + ) = inj.getFullSchedule(); + + for (uint256 i = 0; i < gauges.length; i++) { + assertEq(gauges[i], recipients[i]); + assertEq(amountsPerPeriod[i], 250e18); + assertEq(maxPeriods[i], 5); + assertEq(currentPeriods[i], 0); + assertEq(lastTimestamps[i], uint56(0)); + assertEq(doNotStartBeforeTimestamps[i], uint56(block.timestamp + 1 days)); + } + } +} diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..035de35 --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 035de35f5e366c8d6ed142aec4ccb57fe2dd87d4