From b32630d0be385ba1252c06203841e6f85ded66d4 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:28:19 +0200 Subject: [PATCH 01/31] feat: implement new test structure. --- src/contracts/Interfaces.sol | 7 + test/Base.sol | 47 +++++++ test/fork/concrete/Ownable.t.sol | 50 +++++++ test/fork/concrete/Proxy.t.sol | 70 ++++++++++ .../concrete/SwapExactTokensForTokens.t.sol | 129 ++++++++++++++++++ .../concrete/SwapTokensForExactTokens.t.sol | 128 +++++++++++++++++ test/fork/concrete/Transfer.t.sol | 83 +++++++++++ test/fork/concrete/Withdraw.t.sol | 98 +++++++++++++ test/fork/shared/Shared.sol | 104 ++++++++++++++ test/utils/Addresses.sol | 19 +++ 10 files changed, 735 insertions(+) create mode 100644 test/Base.sol create mode 100644 test/fork/concrete/Ownable.t.sol create mode 100644 test/fork/concrete/Proxy.t.sol create mode 100644 test/fork/concrete/SwapExactTokensForTokens.t.sol create mode 100644 test/fork/concrete/SwapTokensForExactTokens.t.sol create mode 100644 test/fork/concrete/Transfer.t.sol create mode 100644 test/fork/concrete/Withdraw.t.sol create mode 100644 test/fork/shared/Shared.sol create mode 100644 test/utils/Addresses.sol diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 1ddca63..acbecbc 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -9,6 +9,7 @@ interface IERC20 { function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function decimals() external view returns (uint8); + event Transfer(address indexed from, address indexed to, uint256 value); } interface IERC20Metadata is IERC20 { @@ -48,6 +49,12 @@ interface IOETHVault { returns (uint256[] memory amounts, uint256 totalAmount); function addWithdrawalQueueLiquidity() external; + + function setMaxSupplyDiff(uint256 _maxSupplyDiff) external; + + function governor() external returns (address); + + function dripper() external returns (address); } interface IGovernance { diff --git a/test/Base.sol b/test/Base.sol new file mode 100644 index 0000000..c64667b --- /dev/null +++ b/test/Base.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {Test} from "forge-std/Test.sol"; + +// Contracts +import {Proxy} from "contracts/Proxy.sol"; +import {OEthARM} from "contracts/OethARM.sol"; + +// Interfaces +import {IERC20} from "./../src/contracts/Interfaces.sol"; +import {IOETHVault} from "./../src/contracts/Interfaces.sol"; + +abstract contract Base_Test_ is Test { + ////////////////////////////////////////////////////// + /// --- CONTRACTS + ////////////////////////////////////////////////////// + Proxy public proxy; + OEthARM public oethARM; + + ////////////////////////////////////////////////////// + /// --- INTERFACES + ////////////////////////////////////////////////////// + IERC20 public oeth; + IERC20 public weth; + IOETHVault public vault; + + ////////////////////////////////////////////////////// + /// --- EOA + ////////////////////////////////////////////////////// + address public alice; + address public deployer; + address public operator; + address public multisig; + address public strategist; + + ////////////////////////////////////////////////////// + /// --- DEFAULT VALUES + ////////////////////////////////////////////////////// + uint256 public constant DEFAULT_AMOUNT = 1 ether; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual {} +} diff --git a/test/fork/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol new file mode 100644 index 0000000..3e9902a --- /dev/null +++ b/test/fork/concrete/Ownable.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +contract Fork_Concrete_OethARM_Ownable_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_SetOperator_Because_NotOwner() public { + vm.expectRevert("ARM: Only owner can call this function."); + vm.prank(alice); + oethARM.setOperator(deployer); + } + + function test_RevertWhen_SetOwner_Because_NotOwner() public { + vm.expectRevert("ARM: Only owner can call this function."); + vm.prank(alice); + oethARM.setOwner(deployer); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SetOperator() public { + // Assertions before + assertEq(oethARM.operator(), address(0)); + + vm.prank(oethARM.owner()); + oethARM.setOperator(operator); + + // Assertions after + assertEq(oethARM.operator(), operator); + } + + function test_SetOwner() public { + // Assertions before + assertEq(oethARM.owner(), Mainnet.TIMELOCK); + + vm.prank(oethARM.owner()); + oethARM.setOwner(alice); + + // Assertions after + assertEq(oethARM.owner(), alice); + } +} diff --git a/test/fork/concrete/Proxy.t.sol b/test/fork/concrete/Proxy.t.sol new file mode 100644 index 0000000..a746add --- /dev/null +++ b/test/fork/concrete/Proxy.t.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Contracts +import {OEthARM} from "contracts/OethARM.sol"; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +contract Fork_Concrete_OethARM_Proxy_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_UnauthorizedAccess() public { + vm.expectRevert("ARM: Only owner can call this function."); + proxy.setOwner(deployer); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.initialize(address(this), address(this), ""); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.upgradeTo(address(this)); + + vm.expectRevert("ARM: Only owner can call this function."); + proxy.upgradeToAndCall(address(this), ""); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_Upgrade() public { + address owner = Mainnet.TIMELOCK; + + // Deploy new implementation + OEthARM newImplementation = new OEthARM(); + vm.prank(owner); + proxy.upgradeTo(address(newImplementation)); + assertEq(proxy.implementation(), address(newImplementation)); + + // Ensure ownership was preserved. + assertEq(proxy.owner(), owner); + assertEq(oethARM.owner(), owner); + + // Ensure the storage was preserved through the upgrade. + assertEq(address(oethARM.token0()), Mainnet.OETH); + assertEq(address(oethARM.token1()), Mainnet.WETH); + } + + function test_UpgradeAndCall() public { + address owner = Mainnet.TIMELOCK; + + // Deploy new implementation + OEthARM newImplementation = new OEthARM(); + bytes memory data = abi.encodeWithSignature("setOperator(address)", address(0x123)); + + vm.prank(owner); + proxy.upgradeToAndCall(address(newImplementation), data); + assertEq(proxy.implementation(), address(newImplementation)); + + // Ensure ownership was preserved. + assertEq(proxy.owner(), owner); + assertEq(oethARM.owner(), owner); + + // Ensure the post upgrade code was run + assertEq(oethARM.operator(), address(0x123)); + } +} diff --git a/test/fork/concrete/SwapExactTokensForTokens.t.sol b/test/fork/concrete/SwapExactTokensForTokens.t.sol new file mode 100644 index 0000000..75c816d --- /dev/null +++ b/test/fork/concrete/SwapExactTokensForTokens.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + // Deal tokens + deal(address(oeth), address(this), 100 ether); + deal(address(weth), address(oethARM), 100 ether); + deal(address(oeth), address(oethARM), 100 ether); + + // Approve OETH token to ARM contract + oeth.approve(address(oethARM), type(uint256).max); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + + function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InsuficientOutputAmount() public { + vm.expectRevert("ARM: Insufficient output amount"); + oethARM.swapExactTokensForTokens(weth, oeth, 10 ether, 11 ether, address(this)); + } + + function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InvalidSwap_TokenIn() public { + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapExactTokensForTokens(weth, weth, 10 ether, 10 ether, address(this)); + } + + function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InvalidSwap_TokenOut() public { + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapExactTokensForTokens(oeth, oeth, 10 ether, 10 ether, address(this)); + } + + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InsuficientOutputAmount() public { + vm.expectRevert("ARM: Insufficient output amount"); + oethARM.swapExactTokensForTokens(10 ether, 11 ether, new address[](2), address(this), 0); + } + + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidPathLength() public { + vm.expectRevert("ARM: Invalid path length"); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](3), address(this), 0); + } + + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_DeadlineExpired() public { + vm.expectRevert("ARM: Deadline expired"); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), 0); + } + + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalideSwap_TokenIn() public { + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(weth); + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + } + + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalideSwap_TokenOut() public { + address[] memory path = new address[](2); + path[0] = address(oeth); + path[1] = address(oeth); + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapExactTokensForTokens_Simple() public { + // Assertions before + assertEq(weth.balanceOf(address(this)), 0 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 100 ether, "OETH balance user"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); + + // Expected events + vm.expectEmit({emitter: address(oeth)}); + emit IERC20.Transfer(address(this), address(oethARM), 10 ether); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(oethARM), address(this), 10 ether); + // Main call + oethARM.swapExactTokensForTokens(oeth, weth, 10 ether, 10 ether, address(this)); + + // Assertions after + assertEq(weth.balanceOf(address(this)), 10 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 90 ether, "OETH balance"); + assertEq(weth.balanceOf(address(oethARM)), 90 ether, "WETH balance ARM"); + assertEq(oeth.balanceOf(address(oethARM)), 110 ether, "OETH balance ARM"); + } + + function test_SwapExactTokensForTokens_Complex() public { + // Assertions before + assertEq(weth.balanceOf(address(this)), 0 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 100 ether, "OETH balance user"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); + + address[] memory path = new address[](2); + path[0] = address(oeth); + path[1] = address(weth); + + // Expected events + vm.expectEmit({emitter: address(oeth)}); + emit IERC20.Transfer(address(this), address(oethARM), 10 ether); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(oethARM), address(this), 10 ether); + // Main call + uint256[] memory amounts = + oethARM.swapExactTokensForTokens(10 ether, 10 ether, path, address(this), block.timestamp + 1000); + + // Assertions after + assertEq(amounts[0], 10 ether, "Amounts[0]"); + assertEq(amounts[1], 10 ether, "Amounts[1]"); + assertEq(weth.balanceOf(address(this)), 10 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 90 ether, "OETH balance"); + assertEq(weth.balanceOf(address(oethARM)), 90 ether, "WETH balance ARM"); + assertEq(oeth.balanceOf(address(oethARM)), 110 ether, "OETH balance ARM"); + } +} diff --git a/test/fork/concrete/SwapTokensForExactTokens.t.sol b/test/fork/concrete/SwapTokensForExactTokens.t.sol new file mode 100644 index 0000000..d31d490 --- /dev/null +++ b/test/fork/concrete/SwapTokensForExactTokens.t.sol @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + // Deal tokens + deal(address(oeth), address(this), 100 ether); + deal(address(weth), address(oethARM), 100 ether); + deal(address(oeth), address(oethARM), 100 ether); + + // Approve OETH token to ARM contract + oeth.approve(address(oethARM), type(uint256).max); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + + function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InsuficientOutputAmount() public { + vm.expectRevert("ARM: Excess input amount"); + oethARM.swapTokensForExactTokens(weth, oeth, 10 ether, 9 ether, address(this)); + } + + function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InvalidSwap_TokenIn() public { + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); + } + + function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InvalidSwap_TokenOut() public { + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapTokensForExactTokens(oeth, oeth, 10 ether, 10 ether, address(this)); + } + + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InsuficientOutputAmount() public { + vm.expectRevert("ARM: Excess input amount"); + oethARM.swapTokensForExactTokens(10 ether, 9 ether, new address[](2), address(this), 0); + } + + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidPathLength() public { + vm.expectRevert("ARM: Invalid path length"); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](3), address(this), 0); + } + + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_DeadlineExpired() public { + vm.expectRevert("ARM: Deadline expired"); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), 0); + } + + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalideSwap_TokenIn() public { + address[] memory path = new address[](2); + path[0] = address(weth); + path[1] = address(weth); + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + } + + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalideSwap_TokenOut() public { + address[] memory path = new address[](2); + path[0] = address(oeth); + path[1] = address(oeth); + vm.expectRevert("ARM: Invalid swap"); + oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_SwapTokensForExactTokens_Simple() public { + // Assertions before + assertEq(weth.balanceOf(address(this)), 0 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 100 ether, "OETH balance user"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); + + // Expected events + vm.expectEmit({emitter: address(oeth)}); + emit IERC20.Transfer(address(this), address(oethARM), 10 ether); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(oethARM), address(this), 10 ether); + // Main call + oethARM.swapTokensForExactTokens(oeth, weth, 10 ether, 10 ether, address(this)); + + // Assertions after + assertEq(weth.balanceOf(address(this)), 10 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 90 ether, "OETH balance"); + assertEq(weth.balanceOf(address(oethARM)), 90 ether, "WETH balance ARM"); + assertEq(oeth.balanceOf(address(oethARM)), 110 ether, "OETH balance ARM"); + } + + function test_SwapTokensForExactTokens_Complex() public { + // Assertions before + assertEq(weth.balanceOf(address(this)), 0 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 100 ether, "OETH balance user"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "OETH balance ARM"); + assertEq(weth.balanceOf(address(oethARM)), 100 ether, "WETH balance ARM"); + + address[] memory path = new address[](2); + path[0] = address(oeth); + path[1] = address(weth); + // Expected events + vm.expectEmit({emitter: address(oeth)}); + emit IERC20.Transfer(address(this), address(oethARM), 10 ether); + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(oethARM), address(this), 10 ether); + // Main call + uint256[] memory amounts = + oethARM.swapTokensForExactTokens(10 ether, 10 ether, path, address(this), block.timestamp + 1000); + + // Assertions after + assertEq(amounts[0], 10 ether, "Amounts[0]"); + assertEq(amounts[1], 10 ether, "Amounts[1]"); + assertEq(weth.balanceOf(address(this)), 10 ether, "WETH balance user"); + assertEq(oeth.balanceOf(address(this)), 90 ether, "OETH balance"); + assertEq(weth.balanceOf(address(oethARM)), 90 ether, "WETH balance ARM"); + assertEq(oeth.balanceOf(address(oethARM)), 110 ether, "OETH balance ARM"); + } +} diff --git a/test/fork/concrete/Transfer.t.sol b/test/fork/concrete/Transfer.t.sol new file mode 100644 index 0000000..26519da --- /dev/null +++ b/test/fork/concrete/Transfer.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { + bool public shoudRevertOnReceive; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + + function setUp() public override { + super.setUp(); + + // Deal tokens + deal(address(oethARM), 100 ether); + deal(address(weth), address(oethARM), 100 ether); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_TransferToken_Because_NotOwner() public { + vm.expectRevert("ARM: Only owner can call this function."); + oethARM.transferToken(address(0), address(0), 0); + } + + function test_RevertWhen_TransferETH_Because_NotOwner() public { + vm.expectRevert("ARM: Only owner can call this function."); + oethARM.transferEth(address(0), 0); + } + + function test_RevertWhen_TransferETH_Because_ETHTransferFailed() public { + shoudRevertOnReceive = true; + + vm.prank(oethARM.owner()); + vm.expectRevert("ARM: ETH transfer failed"); + oethARM.transferEth(address(this), 10 ether); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_TransferToken() public { + // Assertions before + assertEq(weth.balanceOf(address(this)), 0); + assertEq(weth.balanceOf(address(oethARM)), 100 ether); + + vm.expectEmit({emitter: address(weth)}); + emit IERC20.Transfer(address(oethARM), address(this), 10 ether); + vm.prank(oethARM.owner()); + oethARM.transferToken(address(weth), address(this), 10 ether); + + // Assertions after + assertEq(weth.balanceOf(address(this)), 10 ether); + assertEq(weth.balanceOf(address(oethARM)), 90 ether); + } + + function test_TransferETH() public { + // Assertions before + uint256 balanceBefore = address(this).balance; + assertEq(address(oethARM).balance, 100 ether); + + vm.prank(oethARM.owner()); + oethARM.transferEth(address(this), 10 ether); + + // Assertions after + assertEq(address(this).balance - balanceBefore, 10 ether); + assertEq(address(oethARM).balance, 90 ether); + } + + ////////////////////////////////////////////////////// + /// --- RECEIVE + ////////////////////////////////////////////////////// + receive() external payable { + if (shoudRevertOnReceive) revert(); + } +} diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol new file mode 100644 index 0000000..c9d385c --- /dev/null +++ b/test/fork/concrete/Withdraw.t.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "../shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public override { + super.setUp(); + + // Deal tokens + deal(address(oeth), address(oethARM), 10 ether); + deal(address(weth), address(vault), 10 ether); + + // Remove solvency check + vm.prank(vault.governor()); + vault.setMaxSupplyDiff(0); + + // Bypass call to the dripper + vm.mockCall({callee: vault.dripper(), data: abi.encodeWithSignature("collect()"), returnData: abi.encode(true)}); + } + + ////////////////////////////////////////////////////// + /// --- REVERTING TESTS + ////////////////////////////////////////////////////// + function test_RevertWhen_RequestWithdraw() public { + vm.expectRevert("ARM: Only operator or owner can call this function."); + oethARM.requestWithdrawal(1 ether); + } + + function test_RevertWhen_ClaimWithdraw() public { + vm.expectRevert("ARM: Only operator or owner can call this function."); + oethARM.claimWithdrawal(0); + } + + function test_RevertWhen_ClaimWithdraws() public { + vm.expectRevert("ARM: Only operator or owner can call this function."); + oethARM.claimWithdrawals(new uint256[](0)); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + function test_RequestWithdraw() public { + vm.prank(oethARM.owner()); + vm.expectEmit({emitter: address(oeth)}); + emit IERC20.Transfer(address(oethARM), address(0), 1 ether); + (uint256 requestId, uint256 queued) = oethARM.requestWithdrawal(1 ether); + + // Assertions after + assertEq(requestId, 0, "Request ID should be 0"); + assertEq(queued, 1 ether, "Queued amount should be 1 ether"); + assertEq(oeth.balanceOf(address(oethARM)), 9 ether, "OETH balance should be 99 ether"); + } + + function test_ClaimWithdraw_() public { + // First request withdrawal + vm.prank(oethARM.owner()); + (uint256 requestId,) = oethARM.requestWithdrawal(1 ether); + + vault.addWithdrawalQueueLiquidity(); + skip(10 minutes); // Todo: fetch direct value from contract + + // Then claim withdrawal + vm.prank(oethARM.owner()); + oethARM.claimWithdrawal(requestId); + + // Assertions after + assertEq(weth.balanceOf(address(oethARM)), 1 ether, "WETH balance should be 1 ether"); + } + + function test_ClaimWithdraws() public { + // First request withdrawal + vm.startPrank(oethARM.owner()); + oethARM.requestWithdrawal(1 ether); + oethARM.requestWithdrawal(1 ether); + vm.stopPrank(); + + vault.addWithdrawalQueueLiquidity(); + skip(10 minutes); // Todo: fetch direct value from contract + + uint256[] memory requestIds = new uint256[](2); + requestIds[0] = 0; + requestIds[1] = 1; + // Then claim withdrawal + vm.prank(oethARM.owner()); + oethARM.claimWithdrawals(requestIds); + + // Assertions after + assertEq(weth.balanceOf(address(oethARM)), 2 ether, "WETH balance should be 1 ether"); + } +} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol new file mode 100644 index 0000000..31b3b90 --- /dev/null +++ b/test/fork/shared/Shared.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Base_Test_} from "../../Base.sol"; + +// Contracts +import {Proxy} from "contracts/Proxy.sol"; +import {OEthARM} from "contracts/OethARM.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; +import {IOETHVault} from "contracts/Interfaces.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +abstract contract Fork_Shared_Test_ is Base_Test_ { + uint256 public forkId; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual override { + super.setUp(); + + // 1. Create fork. + _createAndSelectFork(); + + // 2. Create users. + _generateAddresses(); + + // 3. Deploy contracts. + _deployContracts(); + + // 4. Label contracts. + _label(); + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + function _createAndSelectFork() internal { + // Check if the PROVIDER_URL is set. + require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); + + // Create and select a fork. + forkId = vm.createSelectFork(vm.envString("PROVIDER_URL")); + } + + function _generateAddresses() internal { + // Users. + alice = makeAddr("alice"); + deployer = makeAddr("deployer"); + operator = Mainnet.STRATEGIST; + + // Contracts. + oeth = IERC20(Mainnet.OETH); + weth = IERC20(Mainnet.WETH); + vault = IOETHVault(Mainnet.OETHVAULT); + } + + function _deployContracts() internal { + // Deploy Proxy. + proxy = new Proxy(); + + // Deploy OEthARM implementation. + address implementation = address(new OEthARM()); + vm.label(implementation, "OETH ARM IMPLEMENTATION"); + + // Initialize Proxy with OEthARM implementation. + proxy.initialize(implementation, Mainnet.TIMELOCK, ""); + + // Set the Proxy as the OEthARM. + oethARM = OEthARM(address(proxy)); + } + + function _label() internal { + vm.label(address(oeth), "OETH"); + vm.label(address(weth), "WETH"); + vm.label(address(vault), "OETH VAULT"); + vm.label(address(oethARM), "OETH ARM"); + vm.label(address(proxy), "OETH ARM PROXY"); + vm.label(Mainnet.STRATEGIST, "STRATEGIST"); + vm.label(Mainnet.WHALE_OETH, "WHALE OETH"); + vm.label(Mainnet.TIMELOCK, "TIMELOCK"); + vm.label(Mainnet.NULL, "NULL"); + } + + /// @notice Override `deal()` function to handle OETH special case. + function deal(address token, address to, uint256 amount) internal override { + // Handle OETH special case, as rebasing tokens are not supported by the VM. + if (token == address(oeth)) { + // Check than whale as enough OETH. + require(oeth.balanceOf(Mainnet.WHALE_OETH) >= amount, "Fork_Shared_Test_: Not enough OETH in WHALE_OETH"); + + // Transfer OETH from WHALE_OETH to the user. + vm.prank(Mainnet.WHALE_OETH); + oeth.transfer(to, amount); + } else { + super.deal(token, to, amount); + } + } +} diff --git a/test/utils/Addresses.sol b/test/utils/Addresses.sol new file mode 100644 index 0000000..57c1d87 --- /dev/null +++ b/test/utils/Addresses.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +library Mainnet { + ////////////////////////////////////////////////////// + /// --- EOA + ////////////////////////////////////////////////////// + address public constant NULL = address(0); + address public constant STRATEGIST = 0xF14BBdf064E3F67f51cd9BD646aE3716aD938FDC; + address public constant WHALE_OETH = 0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB; + + ////////////////////////////////////////////////////// + /// --- CONTRACTS + ////////////////////////////////////////////////////// + address public constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant TIMELOCK = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; + address public constant OETHVAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; +} From 98cc82983b695e9394ee5f3667ed2ca452b82193 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:29:13 +0200 Subject: [PATCH 02/31] fix: remove previous test structure. --- test/AbstractForkTest.sol | 18 ---- test/OethARM.t.sol | 182 -------------------------------- test/OethLiquidityManager.t.sol | 90 ---------------- test/Proxy.t.sol | 102 ------------------ test/UniswapV2.t.sol | 81 -------------- test/fork/shared/Shared.sol | 104 ------------------ 6 files changed, 577 deletions(-) delete mode 100644 test/AbstractForkTest.sol delete mode 100644 test/OethARM.t.sol delete mode 100644 test/OethLiquidityManager.t.sol delete mode 100644 test/Proxy.t.sol delete mode 100644 test/UniswapV2.t.sol delete mode 100644 test/fork/shared/Shared.sol diff --git a/test/AbstractForkTest.sol b/test/AbstractForkTest.sol deleted file mode 100644 index 1745659..0000000 --- a/test/AbstractForkTest.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test} from "forge-std/Test.sol"; - -import {DeployManager} from "script/deploy/DeployManager.sol"; - -abstract contract AbstractForkTest is Test { - DeployManager internal deployManager; - - constructor() { - deployManager = new DeployManager(); - - // Run deployments - deployManager.setUp(); - deployManager.run(); - } -} diff --git a/test/OethARM.t.sol b/test/OethARM.t.sol deleted file mode 100644 index 72e7d0c..0000000 --- a/test/OethARM.t.sol +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; -import {AbstractForkTest} from "./AbstractForkTest.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; -import {OEthARM} from "contracts/OethARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; -import {Addresses} from "contracts/utils/Addresses.sol"; - -contract OethARMTest is AbstractForkTest { - IERC20 constant weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 constant oeth = IERC20(0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3); - IERC20 BAD_TOKEN = IERC20(makeAddr("bad token")); - - address constant operator = Addresses.STRATEGIST; - - Proxy proxy; - OEthARM oethARM; - - function setUp() public { - proxy = Proxy(deployManager.getDeployment("OETH_ARM")); - oethARM = OEthARM(deployManager.getDeployment("OETH_ARM")); - - _dealWETH(address(oethARM), 100 ether); - _dealOETH(address(oethARM), 100 ether); - - vm.label(address(weth), "WETH"); - vm.label(address(oeth), "OETH"); - - // Only fuzz from this address. Big speedup on fork. - targetSender(address(this)); - } - - function test_swapExactTokensForTokens() external { - _swapExactTokensForTokens(oeth, weth, 10 ether, 10 ether); - } - - function test_swapTokensForExactTokens() external { - _swapTokensForExactTokens(oeth, weth, 10 ether, 10 ether); - } - - function _swapExactTokensForTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); - } else { - _dealOETH(address(this), amountIn + 1000); - } - // Approve the ARM to transfer the input token of the swap. - inToken.approve(address(oethARM), amountIn); - - uint256 startIn = inToken.balanceOf(address(this)); - uint256 startOut = outToken.balanceOf(address(this)); - oethARM.swapExactTokensForTokens(inToken, outToken, amountIn, 0, address(this)); - assertEq(inToken.balanceOf(address(this)), startIn - amountIn, "In actual"); - assertEq(outToken.balanceOf(address(this)), startOut + expectedOut, "Out actual"); - } - - function _swapTokensForExactTokens(IERC20 inToken, IERC20 outToken, uint256 amountIn, uint256 expectedOut) - internal - { - if (inToken == weth) { - _dealWETH(address(this), amountIn + 1000); - } else { - _dealOETH(address(this), amountIn + 1000); - } - // Approve the ARM to transfer the input token of the swap. - inToken.approve(address(oethARM), amountIn); - - uint256 startIn = inToken.balanceOf(address(this)); - - oethARM.swapTokensForExactTokens(inToken, outToken, expectedOut, 3 * expectedOut, address(this)); - assertEq(inToken.balanceOf(address(this)), startIn - amountIn, "In actual"); - assertEq(outToken.balanceOf(address(this)), expectedOut, "Out actual"); - } - - function test_unauthorizedAccess() external { - address RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; - vm.startPrank(RANDOM_ADDRESS); - - // Proxy's restricted methods. - vm.expectRevert("ARM: Only owner can call this function."); - proxy.setOwner(RANDOM_ADDRESS); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.initialize(address(this), address(this), ""); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeTo(address(this)); - - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeToAndCall(address(this), ""); - - // Implementation's restricted methods. - vm.expectRevert("ARM: Only owner can call this function."); - oethARM.setOwner(RANDOM_ADDRESS); - } - - function test_wrongInTokenExactIn() external { - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(BAD_TOKEN, oeth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(BAD_TOKEN, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(weth, weth, 10 ether, 0, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapExactTokensForTokens(oeth, oeth, 10 ether, 0, address(this)); - } - - function test_wrongOutTokenExactIn() external { - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(oeth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(oeth, oeth, 10 ether, 10 ether, address(this)); - } - - function test_wrongInTokenExactOut() external { - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(BAD_TOKEN, oeth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(BAD_TOKEN, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(oeth, oeth, 10 ether, 10 ether, address(this)); - } - - function test_wrongOutTokenExactOut() external { - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(weth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(oeth, BAD_TOKEN, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(weth, weth, 10 ether, 10 ether, address(this)); - vm.expectRevert("ARM: Invalid swap"); - oethARM.swapTokensForExactTokens(oeth, oeth, 10 ether, 10 ether, address(this)); - } - - function test_collectTokens() external { - vm.startPrank(Addresses.TIMELOCK); - - oethARM.transferToken(address(weth), address(this), weth.balanceOf(address(oethARM))); - assertGt(weth.balanceOf(address(this)), 50 ether); - assertEq(weth.balanceOf(address(oethARM)), 0); - - oethARM.transferToken(address(oeth), address(this), oeth.balanceOf(address(oethARM))); - assertGt(oeth.balanceOf(address(this)), 50 ether); - assertLt(oeth.balanceOf(address(oethARM)), 3); - - vm.stopPrank(); - } - - function _dealOETH(address to, uint256 amount) internal { - vm.prank(0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB); - oeth.transfer(to, amount); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); - } - - /* Operator Tests */ - - function test_setOperator() external { - vm.prank(Addresses.TIMELOCK); - oethARM.setOperator(address(this)); - assertEq(oethARM.operator(), address(this)); - } - - function test_nonOwnerCannotSetOperator() external { - vm.expectRevert("ARM: Only owner can call this function."); - vm.prank(operator); - oethARM.setOperator(operator); - } -} diff --git a/test/OethLiquidityManager.t.sol b/test/OethLiquidityManager.t.sol deleted file mode 100644 index 1a1eb6d..0000000 --- a/test/OethLiquidityManager.t.sol +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {console2} from "forge-std/Test.sol"; -import {AbstractForkTest} from "./AbstractForkTest.sol"; - -import {IERC20, IOethARM, IOETHVault} from "contracts/Interfaces.sol"; -import {OEthARM} from "contracts/OethARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; -import {Addresses} from "contracts/utils/Addresses.sol"; - -contract OethLiquidityManagerTest is AbstractForkTest { - address constant RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; - - address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address constant OETH = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; - IERC20 constant oeth = IERC20(OETH); - IERC20 constant weth = IERC20(WETH); - - IOETHVault constant vault = IOETHVault(0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab); - - Proxy proxy; - OEthARM oethARM; - - address constant operator = Addresses.STRATEGIST; - - function setUp() public { - vm.label(WETH, "WETH"); - vm.label(OETH, "OETH"); - - proxy = Proxy(deployManager.getDeployment("OETH_ARM")); - oethARM = OEthARM(deployManager.getDeployment("OETH_ARM")); - } - - function test_withdrawal() external { - uint256 amount = 1 ether; - _dealOEth(address(proxy), 10 ether); - // put some WETH in the vault - _dealWEth(address(vault), 10 ether); - - vm.startPrank(operator); - (uint256 requestId, uint256 queued) = oethARM.requestWithdrawal(1 ether); - - // Snapshot WETH balance - uint256 startBalance = weth.balanceOf(address(oethARM)); - - vault.addWithdrawalQueueLiquidity(); - - skip(10 minutes); - - // Claim the ETH. - oethARM.claimWithdrawal(requestId); - - // Ensure the balance increased. - assertGt(weth.balanceOf(address(oethARM)), startBalance, "Withdrawal did not increase WETH balance"); - - vm.stopPrank(); - } - - /* - * Admin tests. - * - */ - function test_unauthorizedAccess() external { - vm.startPrank(RANDOM_ADDRESS); - - vm.expectRevert("ARM: Only operator or owner can call this function."); - oethARM.requestWithdrawal(1 ether); - - vm.expectRevert("ARM: Only operator or owner can call this function."); - oethARM.claimWithdrawal(1); - - uint256[] memory requestIds = new uint256[](2); - requestIds[0] = 10; - requestIds[1] = 22; - - vm.expectRevert("ARM: Only operator or owner can call this function."); - oethARM.claimWithdrawals(requestIds); - } - - function _dealOEth(address to, uint256 amount) internal { - vm.prank(0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB); // OETH whale - oeth.transfer(to, amount); - } - - function _dealWEth(address to, uint256 amount) internal { - vm.prank(0xF04a5cC80B1E94C69B48f5ee68a08CD2F09A7c3E); // WETH whale - weth.transfer(to, amount); - } -} diff --git a/test/Proxy.t.sol b/test/Proxy.t.sol deleted file mode 100644 index 9f8cff7..0000000 --- a/test/Proxy.t.sol +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Vm} from "forge-std/Vm.sol"; -import {console2} from "forge-std/Test.sol"; -import {AbstractForkTest} from "./AbstractForkTest.sol"; - -import {OEthARM} from "contracts/OethARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; -import {Addresses} from "contracts/utils/Addresses.sol"; - -contract ProxyTest is AbstractForkTest { - address constant RANDOM_ADDRESS = 0xfEEDBeef00000000000000000000000000000000; - - Proxy proxy; - OEthARM oethARM; - - address constant weth = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - address constant oeth = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; - - address constant owner = Addresses.TIMELOCK; - address constant operator = Addresses.STRATEGIST; - - function setUp() public { - vm.label(weth, "WETH"); - vm.label(oeth, "OETH"); - - proxy = Proxy(deployManager.getDeployment("OETH_ARM")); - oethARM = OEthARM(deployManager.getDeployment("OETH_ARM")); - } - - function test_upgrade() external { - OEthARM newImplementation1 = new OEthARM(); - vm.prank(owner); - proxy.upgradeTo(address(newImplementation1)); - assertEq(proxy.implementation(), address(newImplementation1)); - - // Ensure ownership was preserved. - assertEq(proxy.owner(), owner); - assertEq(oethARM.owner(), owner); - - // Ensure the storage was preserved through the upgrade. - assertEq(address(oethARM.token0()), oeth); - assertEq(address(oethARM.token1()), weth); - } - - function test_upgradeAndCall() external { - OEthARM newImplementation2 = new OEthARM(); - bytes memory data = abi.encodeWithSignature("setOperator(address)", address(0x123)); - - vm.prank(owner); - proxy.upgradeToAndCall(address(newImplementation2), data); - assertEq(proxy.implementation(), address(newImplementation2)); - - // Ensure ownership was preserved. - assertEq(proxy.owner(), owner); - assertEq(oethARM.owner(), owner); - - // Ensure the post upgrade code was run - assertEq(oethARM.operator(), address(0x123)); - } - - function test_setOwner() external { - assertEq(proxy.owner(), owner); - assertEq(oethARM.owner(), owner); - - // Update the owner. - address newOwner = RANDOM_ADDRESS; - vm.prank(owner); - proxy.setOwner(newOwner); - assertEq(proxy.owner(), newOwner); - assertEq(oethARM.owner(), newOwner); - - // Old owner (this) should now be unauthorized. - vm.expectRevert("ARM: Only owner can call this function."); - oethARM.setOwner(address(this)); - } - - function test_unauthorizedAccess() external { - // Proxy's restricted methods. - vm.prank(RANDOM_ADDRESS); - vm.expectRevert("ARM: Only owner can call this function."); - proxy.setOwner(RANDOM_ADDRESS); - - vm.prank(RANDOM_ADDRESS); - vm.expectRevert("ARM: Only owner can call this function."); - proxy.initialize(address(this), address(this), ""); - - vm.prank(RANDOM_ADDRESS); - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeTo(address(this)); - - vm.prank(RANDOM_ADDRESS); - vm.expectRevert("ARM: Only owner can call this function."); - proxy.upgradeToAndCall(address(this), ""); - - // Implementation's restricted methods. - vm.prank(RANDOM_ADDRESS); - vm.expectRevert("ARM: Only owner can call this function."); - oethARM.setOwner(RANDOM_ADDRESS); - } -} diff --git a/test/UniswapV2.t.sol b/test/UniswapV2.t.sol deleted file mode 100644 index 153a906..0000000 --- a/test/UniswapV2.t.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.23; - -import {Test, console2} from "forge-std/Test.sol"; -import {AbstractForkTest} from "./AbstractForkTest.sol"; - -import {IERC20} from "contracts/Interfaces.sol"; -import {OEthARM} from "contracts/OethARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; -import {Addresses} from "contracts/utils/Addresses.sol"; - -// Tests for the Uniswap V2 Router compatible interface of OSwap. -contract UniswapV2Test is AbstractForkTest { - IERC20 weth = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); - IERC20 oeth = IERC20(0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3); - - address constant operator = Addresses.STRATEGIST; - - Proxy proxy; - OEthARM oethARM; - - function setUp() public { - proxy = Proxy(deployManager.getDeployment("OETH_ARM")); - oethARM = OEthARM(deployManager.getDeployment("OETH_ARM")); - - // Add liquidity to the test contract. - _dealWETH(address(this), 120 ether); - _dealOEth(address(this), 120 ether); - - // Add liquidity to the pool. - _dealWETH(address(oethARM), 120 ether); - _dealOEth(address(oethARM), 120 ether); - - weth.approve(address(oethARM), type(uint256).max); - oeth.approve(address(oethARM), type(uint256).max); - vm.label(address(weth), "WETH"); - vm.label(address(oeth), "stETH"); - } - - function _dealOEth(address to, uint256 amount) internal { - vm.prank(0x8E02247D3eE0E6153495c971FFd45Aa131f4D7cB); // OETH whale - oeth.transfer(to, amount); - } - - function _dealWETH(address to, uint256 amount) internal { - deal(address(weth), to, amount); - } - - function test_swapExactOEthForWeth() external { - address[] memory path = new address[](2); - path[0] = address(oeth); - path[1] = address(weth); - uint256 balanceBefore = weth.balanceOf(address(this)); - - uint256[] memory amounts = oethARM.swapExactTokensForTokens(100 ether, 99, path, address(this), block.timestamp); - - assertGt(amounts[0], 0, "amount[0] should not be zero"); - assertGt(amounts[1], 0, "amount[1] should not be zero"); - assertGe(weth.balanceOf(address(this)), balanceBefore + amounts[1], "received all output amount"); - } - - function test_swapStEthForExactWeth() external { - address[] memory path = new address[](2); - path[0] = address(oeth); - path[1] = address(weth); - uint256 balanceBefore = weth.balanceOf(address(this)); - - uint256[] memory amounts = - oethARM.swapTokensForExactTokens(100 ether, 101 ether, path, address(this), block.timestamp); - - assertGt(amounts[0], 0, "amount[0] should not be zero"); - assertGt(amounts[1], 0, "amount[1] should not be zero"); - assertGe(weth.balanceOf(address(this)), balanceBefore + amounts[1], "received all output amount"); - } - - function test_deadline() external { - address[] memory path = new address[](2); - vm.expectRevert("ARM: Deadline expired"); - oethARM.swapExactTokensForTokens(0, 0, path, address(this), block.timestamp - 1); - } -} diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol deleted file mode 100644 index 31b3b90..0000000 --- a/test/fork/shared/Shared.sol +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -// Test imports -import {Base_Test_} from "../../Base.sol"; - -// Contracts -import {Proxy} from "contracts/Proxy.sol"; -import {OEthARM} from "contracts/OethARM.sol"; - -// Interfaces -import {IERC20} from "contracts/Interfaces.sol"; -import {IOETHVault} from "contracts/Interfaces.sol"; - -// Utils -import {Mainnet} from "test/utils/Addresses.sol"; - -abstract contract Fork_Shared_Test_ is Base_Test_ { - uint256 public forkId; - - ////////////////////////////////////////////////////// - /// --- SETUP - ////////////////////////////////////////////////////// - function setUp() public virtual override { - super.setUp(); - - // 1. Create fork. - _createAndSelectFork(); - - // 2. Create users. - _generateAddresses(); - - // 3. Deploy contracts. - _deployContracts(); - - // 4. Label contracts. - _label(); - } - - ////////////////////////////////////////////////////// - /// --- HELPERS - ////////////////////////////////////////////////////// - function _createAndSelectFork() internal { - // Check if the PROVIDER_URL is set. - require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); - - // Create and select a fork. - forkId = vm.createSelectFork(vm.envString("PROVIDER_URL")); - } - - function _generateAddresses() internal { - // Users. - alice = makeAddr("alice"); - deployer = makeAddr("deployer"); - operator = Mainnet.STRATEGIST; - - // Contracts. - oeth = IERC20(Mainnet.OETH); - weth = IERC20(Mainnet.WETH); - vault = IOETHVault(Mainnet.OETHVAULT); - } - - function _deployContracts() internal { - // Deploy Proxy. - proxy = new Proxy(); - - // Deploy OEthARM implementation. - address implementation = address(new OEthARM()); - vm.label(implementation, "OETH ARM IMPLEMENTATION"); - - // Initialize Proxy with OEthARM implementation. - proxy.initialize(implementation, Mainnet.TIMELOCK, ""); - - // Set the Proxy as the OEthARM. - oethARM = OEthARM(address(proxy)); - } - - function _label() internal { - vm.label(address(oeth), "OETH"); - vm.label(address(weth), "WETH"); - vm.label(address(vault), "OETH VAULT"); - vm.label(address(oethARM), "OETH ARM"); - vm.label(address(proxy), "OETH ARM PROXY"); - vm.label(Mainnet.STRATEGIST, "STRATEGIST"); - vm.label(Mainnet.WHALE_OETH, "WHALE OETH"); - vm.label(Mainnet.TIMELOCK, "TIMELOCK"); - vm.label(Mainnet.NULL, "NULL"); - } - - /// @notice Override `deal()` function to handle OETH special case. - function deal(address token, address to, uint256 amount) internal override { - // Handle OETH special case, as rebasing tokens are not supported by the VM. - if (token == address(oeth)) { - // Check than whale as enough OETH. - require(oeth.balanceOf(Mainnet.WHALE_OETH) >= amount, "Fork_Shared_Test_: Not enough OETH in WHALE_OETH"); - - // Transfer OETH from WHALE_OETH to the user. - vm.prank(Mainnet.WHALE_OETH); - oeth.transfer(to, amount); - } else { - super.deal(token, to, amount); - } - } -} From efc9976da780fe5550236b97adf839fe44067873 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:30:39 +0200 Subject: [PATCH 03/31] build: fix CI. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a2dff6..9440355 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,4 +41,4 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run fork tests - run: forge test --fork-url $PROVIDER_URL -vvv + run: forge test -vvv From e82e2bb9ebda567c14e31ec6869ea571a8444bdc Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:34:59 +0200 Subject: [PATCH 04/31] fix: import missing file. --- test/fork/shared/Shared.sol | 104 ++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 test/fork/shared/Shared.sol diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol new file mode 100644 index 0000000..62c36f9 --- /dev/null +++ b/test/fork/shared/Shared.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Base_Test_} from "test/Base.sol"; + +// Contracts +import {Proxy} from "contracts/Proxy.sol"; +import {OEthARM} from "contracts/OethARM.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; +import {IOETHVault} from "contracts/Interfaces.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +abstract contract Fork_Shared_Test_ is Base_Test_ { + uint256 public forkId; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual override { + super.setUp(); + + // 1. Create fork. + _createAndSelectFork(); + + // 2. Create users. + _generateAddresses(); + + // 3. Deploy contracts. + _deployContracts(); + + // 4. Label contracts. + _label(); + } + + ////////////////////////////////////////////////////// + /// --- HELPERS + ////////////////////////////////////////////////////// + function _createAndSelectFork() internal { + // Check if the PROVIDER_URL is set. + require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); + + // Create and select a fork. + forkId = vm.createSelectFork(vm.envString("PROVIDER_URL")); + } + + function _generateAddresses() internal { + // Users. + alice = makeAddr("alice"); + deployer = makeAddr("deployer"); + operator = Mainnet.STRATEGIST; + + // Contracts. + oeth = IERC20(Mainnet.OETH); + weth = IERC20(Mainnet.WETH); + vault = IOETHVault(Mainnet.OETHVAULT); + } + + function _deployContracts() internal { + // Deploy Proxy. + proxy = new Proxy(); + + // Deploy OEthARM implementation. + address implementation = address(new OEthARM()); + vm.label(implementation, "OETH ARM IMPLEMENTATION"); + + // Initialize Proxy with OEthARM implementation. + proxy.initialize(implementation, Mainnet.TIMELOCK, ""); + + // Set the Proxy as the OEthARM. + oethARM = OEthARM(address(proxy)); + } + + function _label() internal { + vm.label(address(oeth), "OETH"); + vm.label(address(weth), "WETH"); + vm.label(address(vault), "OETH VAULT"); + vm.label(address(oethARM), "OETH ARM"); + vm.label(address(proxy), "OETH ARM PROXY"); + vm.label(Mainnet.STRATEGIST, "STRATEGIST"); + vm.label(Mainnet.WHALE_OETH, "WHALE OETH"); + vm.label(Mainnet.TIMELOCK, "TIMELOCK"); + vm.label(Mainnet.NULL, "NULL"); + } + + /// @notice Override `deal()` function to handle OETH special case. + function deal(address token, address to, uint256 amount) internal override { + // Handle OETH special case, as rebasing tokens are not supported by the VM. + if (token == address(oeth)) { + // Check than whale as enough OETH. + require(oeth.balanceOf(Mainnet.WHALE_OETH) >= amount, "Fork_Shared_Test_: Not enough OETH in WHALE_OETH"); + + // Transfer OETH from WHALE_OETH to the user. + vm.prank(Mainnet.WHALE_OETH); + oeth.transfer(to, amount); + } else { + super.deal(token, to, amount); + } + } +} From 55fecc6fef38a042cb1507cf885f8651f7b05f83 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:35:07 +0200 Subject: [PATCH 05/31] fix: import path. --- test/Base.sol | 4 ++-- test/fork/concrete/Ownable.t.sol | 2 +- test/fork/concrete/Proxy.t.sol | 2 +- test/fork/concrete/SwapExactTokensForTokens.t.sol | 2 +- test/fork/concrete/SwapTokensForExactTokens.t.sol | 2 +- test/fork/concrete/Transfer.t.sol | 2 +- test/fork/concrete/Withdraw.t.sol | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/Base.sol b/test/Base.sol index c64667b..478e029 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -9,8 +9,8 @@ import {Proxy} from "contracts/Proxy.sol"; import {OEthARM} from "contracts/OethARM.sol"; // Interfaces -import {IERC20} from "./../src/contracts/Interfaces.sol"; -import {IOETHVault} from "./../src/contracts/Interfaces.sol"; +import {IERC20} from "contracts/Interfaces.sol"; +import {IOETHVault} from "contracts/Interfaces.sol"; abstract contract Base_Test_ is Test { ////////////////////////////////////////////////////// diff --git a/test/fork/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol index 3e9902a..a44e064 100644 --- a/test/fork/concrete/Ownable.t.sol +++ b/test/fork/concrete/Ownable.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; diff --git a/test/fork/concrete/Proxy.t.sol b/test/fork/concrete/Proxy.t.sol index a746add..e643fff 100644 --- a/test/fork/concrete/Proxy.t.sol +++ b/test/fork/concrete/Proxy.t.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import {OEthARM} from "contracts/OethARM.sol"; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; diff --git a/test/fork/concrete/SwapExactTokensForTokens.t.sol b/test/fork/concrete/SwapExactTokensForTokens.t.sol index 75c816d..0cb6a52 100644 --- a/test/fork/concrete/SwapExactTokensForTokens.t.sol +++ b/test/fork/concrete/SwapExactTokensForTokens.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; diff --git a/test/fork/concrete/SwapTokensForExactTokens.t.sol b/test/fork/concrete/SwapTokensForExactTokens.t.sol index d31d490..04d46fc 100644 --- a/test/fork/concrete/SwapTokensForExactTokens.t.sol +++ b/test/fork/concrete/SwapTokensForExactTokens.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; diff --git a/test/fork/concrete/Transfer.t.sol b/test/fork/concrete/Transfer.t.sol index 26519da..248b364 100644 --- a/test/fork/concrete/Transfer.t.sol +++ b/test/fork/concrete/Transfer.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index c9d385c..9950a74 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Fork_Shared_Test_} from "../shared/Shared.sol"; +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; From 92997d5df9294376bbe933eea9d4516faa3becb7 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:36:51 +0200 Subject: [PATCH 06/31] forge fmt --- src/contracts/Interfaces.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index acbecbc..5a5ad53 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -9,6 +9,7 @@ interface IERC20 { function approve(address spender, uint256 value) external returns (bool); function transferFrom(address from, address to, uint256 value) external returns (bool); function decimals() external view returns (uint8); + event Transfer(address indexed from, address indexed to, uint256 value); } From a7d9afbb6bb5b35ab048b09798b5eecd19b50bae Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 14:39:08 +0200 Subject: [PATCH 07/31] build: improve CI. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9440355..532168b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,4 +41,4 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Run fork tests - run: forge test -vvv + run: forge test -vvv --summary --detailed From 01042b99e3dc7608a17a3851b0d6e782eddea476 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 15:54:53 +0200 Subject: [PATCH 08/31] feat: decoupling deployment script from test. --- build/.gitkeep | 0 build/deployments.json | 1 - script/001_OETH_ARM.s.sol | 38 +++++ script/deploy/DeployManager.sol | 170 -------------------- script/deploy/mainnet/001_DeployCore.sol | 55 ------- script/deploy/mainnet/BaseMainnetScript.sol | 102 ------------ 6 files changed, 38 insertions(+), 328 deletions(-) delete mode 100644 build/.gitkeep delete mode 100644 build/deployments.json create mode 100644 script/001_OETH_ARM.s.sol delete mode 100644 script/deploy/DeployManager.sol delete mode 100644 script/deploy/mainnet/001_DeployCore.sol delete mode 100644 script/deploy/mainnet/BaseMainnetScript.sol diff --git a/build/.gitkeep b/build/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/build/deployments.json b/build/deployments.json deleted file mode 100644 index 5eba78b..0000000 --- a/build/deployments.json +++ /dev/null @@ -1 +0,0 @@ -{ "1": { "executions": {}, "contracts": {} } } \ No newline at end of file diff --git a/script/001_OETH_ARM.s.sol b/script/001_OETH_ARM.s.sol new file mode 100644 index 0000000..32025dd --- /dev/null +++ b/script/001_OETH_ARM.s.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {Test} from "forge-std/Test.sol"; +import {Script} from "forge-std/Script.sol"; + +// Contracts +import {Proxy} from "contracts/Proxy.sol"; +import {OEthARM} from "contracts/OethARM.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +contract OETHARMScript is Script { + // Fetch PK from env and derive the deployer address + address public deployer = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function run() public { + // 🟡 All the next transactions will be sent by the deployer if `--broadcast`option is used on the command line 🟡 + vm.startBroadcast(deployer); + + // 1. Deploy proxy contracts + Proxy proxy = new Proxy(); + + // 2. Deploy implementation + OEthARM oethARMImple = new OEthARM(); + + // 3. Initialize proxy + proxy.initialize(address(oethARMImple), Mainnet.TIMELOCK, ""); + + // Stop broadcasting + vm.stopBroadcast(); + } +} diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol deleted file mode 100644 index 9f32d7c..0000000 --- a/script/deploy/DeployManager.sol +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: Unlicense -pragma solidity 0.8.23; - -import "forge-std/Script.sol"; - -import {BaseMainnetScript} from "./mainnet/BaseMainnetScript.sol"; - -import {DeployCoreScript} from "./mainnet/001_DeployCore.sol"; - -import {VmSafe} from "forge-std/Vm.sol"; - -contract DeployManager is Script { - mapping(string => address) public deployedContracts; - mapping(string => bool) public scriptsExecuted; - - string internal forkFileId = ""; - - bool public isForked; - - constructor() { - isForked = vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); - forkFileId = vm.toString(block.timestamp); - } - - function getDeploymentFilePath() public view returns (string memory) { - return isForked ? getForkDeploymentFilePath() : getMainnetDeploymentFilePath(); - } - - function getMainnetDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments.json")); - } - - function getForkDeploymentFilePath() public view returns (string memory) { - return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork", forkFileId, ".json")); - } - - function setForkFileId(string memory _forkFileId) external { - forkFileId = _forkFileId; - } - - function setUp() external { - string memory chainIdStr = vm.toString(block.chainid); - string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); - - string memory mainnetFilePath = getMainnetDeploymentFilePath(); - if (!vm.isFile(mainnetFilePath)) { - // Create mainnet deployment file if it doesn't exist - vm.writeFile( - mainnetFilePath, - string(abi.encodePacked('{ "', chainIdStr, '": { "executions": {}, "contracts": {} } }')) - ); - } else if (!vm.keyExistsJson(vm.readFile(mainnetFilePath), chainIdKey)) { - // Create network entry if it doesn't exist - vm.writeJson( - vm.serializeJson(chainIdStr, '{ "executions": {}, "contracts": {} }'), mainnetFilePath, chainIdKey - ); - } - - if (isForked) { - // Duplicate Mainnet File - vm.writeFile(getForkDeploymentFilePath(), vm.readFile(mainnetFilePath)); - } - } - - function run() external { - // TODO: Use vm.readDir to recursively build this? - _runDeployFile(new DeployCoreScript()); - } - - function _runDeployFile(BaseMainnetScript deployScript) internal { - if (deployScript.proposalExecuted()) { - // No action to do - return; - } else if (deployScript.skip()) { - console.log("Skipping deployment (skip() == true)"); - return; - } - - string memory chainIdStr = vm.toString(block.chainid); - string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); - - string memory contractsKey = string(abi.encodePacked(chainIdKey, ".contracts")); - string memory executionsKey = string(abi.encodePacked(chainIdKey, ".executions")); - - string memory deploymentsFilePath = getDeploymentFilePath(); - string memory fileContents = vm.readFile(deploymentsFilePath); - - /** - * Execution History - */ - string memory currentExecutions = ""; - string[] memory executionKeys = vm.parseJsonKeys(fileContents, executionsKey); - - for (uint256 i = 0; i < executionKeys.length; ++i) { - uint256 deployedTimestamp = - vm.parseJsonUint(fileContents, string(abi.encodePacked(executionsKey, ".", executionKeys[i]))); - - currentExecutions = vm.serializeUint(executionsKey, executionKeys[i], deployedTimestamp); - scriptsExecuted[executionKeys[i]] = true; - } - - /** - * Pre-deployment - */ - string memory networkDeployments = ""; - string[] memory existingContracts = vm.parseJsonKeys(fileContents, contractsKey); - for (uint256 i = 0; i < existingContracts.length; ++i) { - address deployedAddr = - vm.parseJsonAddress(fileContents, string(abi.encodePacked(contractsKey, ".", existingContracts[i]))); - - networkDeployments = vm.serializeAddress(contractsKey, existingContracts[i], deployedAddr); - - deployedContracts[existingContracts[i]] = deployedAddr; - - deployScript.preloadDeployedContract(existingContracts[i], deployedAddr); - } - - if (scriptsExecuted[deployScript.DEPLOY_NAME()]) { - console.log("Skipping deployment (already deployed)"); - - // Governance handling - deployScript.handleGovernanceProposal(); - } else { - // Deployment - deployScript.setUp(); - deployScript.run(); - - /** - * Post-deployment - */ - BaseMainnetScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); - - for (uint256 i = 0; i < records.length; ++i) { - string memory name = records[i].name; - address addr = records[i].addr; - - console.log(string(abi.encodePacked("> Recorded Deploy of ", name, " at")), addr); - networkDeployments = vm.serializeAddress(contractsKey, name, addr); - deployedContracts[name] = addr; - } - - /** - * Write Execution History - */ - currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); - - // Write to file instead of using writeJson to avoid "EOF while parsing a value at line 1 column 0" error. - vm.writeFile( - getForkDeploymentFilePath(), - string( - abi.encodePacked( - '{ "', - chainIdStr, - '": { "executions": ', - currentExecutions, - ', "contracts": ', - networkDeployments, - "}}" - ) - ) - ); - - console.log("> Deployment addresses stored and Deploy script execution complete."); - } - } - - function getDeployment(string calldata contractName) external view returns (address) { - return deployedContracts[contractName]; - } -} diff --git a/script/deploy/mainnet/001_DeployCore.sol b/script/deploy/mainnet/001_DeployCore.sol deleted file mode 100644 index 521b6f7..0000000 --- a/script/deploy/mainnet/001_DeployCore.sol +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity 0.8.23; - -import "./BaseMainnetScript.sol"; -import {Vm} from "forge-std/Vm.sol"; - -import {Addresses} from "contracts/utils/Addresses.sol"; - -import {OEthARM} from "contracts/OethARM.sol"; -import {Proxy} from "contracts/Proxy.sol"; - -import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; - -contract DeployCoreScript is BaseMainnetScript { - using GovSixHelper for GovProposal; - - GovProposal public govProposal; - - string public constant override DEPLOY_NAME = "000_DeployCoreScript"; - bool public constant override proposalExecuted = false; - - constructor() {} - - function _execute() internal override { - console.log("Deploy:"); - console.log("------------"); - - // 1. Deploy proxy contracts - Proxy proxy = new Proxy(); - _recordDeploy("OETH_ARM", address(proxy)); - - // 2. Deploy implementation - OEthARM implementation = new OEthARM(); - _recordDeploy("OETH_ARM_IMPL", address(implementation)); - - // 3. Initialize proxy - proxy.initialize(address(implementation), Addresses.TIMELOCK, ""); - } - - function _buildGovernanceProposal() internal override { - govProposal.setDescription("Setup OETH ARM Contract"); - - // NOTE: This could be done during deploy of proxy. - // But doing this here to test governance flow. - - // Set operator - govProposal.action(deployedContracts["OETH_ARM"], "setOperator(address)", abi.encode(Addresses.STRATEGIST)); - } - - function _fork() internal override { - // Simulate on fork - govProposal.simulate(); - } -} diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol deleted file mode 100644 index d8f0495..0000000 --- a/script/deploy/mainnet/BaseMainnetScript.sol +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.23; - -import "forge-std/console.sol"; - -import {Script} from "forge-std/Script.sol"; -import {Vm, VmSafe} from "forge-std/Vm.sol"; - -import {Addresses} from "contracts/utils/Addresses.sol"; -import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; - -abstract contract BaseMainnetScript is Script { - using GovSixHelper for GovProposal; - - uint256 public deployBlockNum = type(uint256).max; - - // DeployerRecord stuff to be extracted as well - struct DeployRecord { - string name; - address addr; - } - - DeployRecord[] public deploys; - - mapping(string => address) public deployedContracts; - - function _recordDeploy(string memory name, address addr) internal { - deploys.push(DeployRecord({name: name, addr: addr})); - console.log(string(abi.encodePacked("> Deployed ", name, " at")), addr); - deployedContracts[name] = addr; - } - // End DeployRecord - - function getAllDeployRecords() external view returns (DeployRecord[] memory) { - return deploys; - } - - function preloadDeployedContract(string memory name, address addr) external { - deployedContracts[name] = addr; - } - - function isForked() public view returns (bool) { - return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); - } - - function setUp() external {} - - function run() external { - if (block.chainid != 1) { - revert("Not Mainnet"); - } - // Will not execute script if after this block number - if (block.number > deployBlockNum) { - // console.log("Current block %s, script block %s", block.number, deployBlockNum); - return; - } - - if (this.isForked()) { - address impersonator = Addresses.INITIAL_DEPLOYER; - console.log("Running script on mainnet fork impersonating: %s", impersonator); - vm.startPrank(impersonator); - } else { - uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); - address deployer = vm.rememberKey(deployerPrivateKey); - vm.startBroadcast(deployer); - console.log("Deploying on mainnet with deployer: %s", deployer); - } - - _execute(); - - if (this.isForked()) { - vm.stopPrank(); - _buildGovernanceProposal(); - _fork(); - } else { - vm.stopBroadcast(); - } - } - - function DEPLOY_NAME() external view virtual returns (string memory); - - function proposalExecuted() external view virtual returns (bool); - - function skip() external view virtual returns (bool) { - return false; - } - - function _execute() internal virtual; - - function _fork() internal virtual; - - function _buildGovernanceProposal() internal virtual {} - - function handleGovernanceProposal() external virtual { - if (this.proposalExecuted()) { - return; - } - - _buildGovernanceProposal(); - _fork(); - } -} From c09e84938fcd11bac14e14ef61b9875318befb3c Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 16:23:34 +0200 Subject: [PATCH 09/31] chore: add verbosity on `.toml`. --- foundry.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/foundry.toml b/foundry.toml index 6469e6d..585b99d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,6 +2,7 @@ src = "src/contracts" out = "out" libs = ["lib"] +verbosity = 3 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options From ef043d57fa1671e2a4a3784da86e90b6554620ac Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 16:23:47 +0200 Subject: [PATCH 10/31] feat: improve deployment script. --- script/001_OETH_ARM.s.sol | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/script/001_OETH_ARM.s.sol b/script/001_OETH_ARM.s.sol index 32025dd..927cc8d 100644 --- a/script/001_OETH_ARM.s.sol +++ b/script/001_OETH_ARM.s.sol @@ -2,7 +2,6 @@ pragma solidity 0.8.23; // Foundry -import {Test} from "forge-std/Test.sol"; import {Script} from "forge-std/Script.sol"; // Contracts @@ -12,13 +11,29 @@ import {OEthARM} from "contracts/OethARM.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; -contract OETHARMScript is Script { - // Fetch PK from env and derive the deployer address - address public deployer = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); +/// @notice Deploy the OEthARM contract using a proxy. +/// @dev 1. Deploy the proxy contract. +/// 2. Deploy the OEthARM implementation contract. +/// 3. Initialize the proxy contract with the OEthARM implementation contract. +contract _001_OETHARMScript is Script { + address public deployer; ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// + function setUp() public { + if (vm.envExists("DEPLOYER_PRIVATE_KEY")) { + // Fetch PK from env and derive the deployer address + deployer = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); + } else { + // If no PK is provided, use a default deployer address + deployer = makeAddr("deployer"); + } + } + + ////////////////////////////////////////////////////// + /// --- RUN + ////////////////////////////////////////////////////// function run() public { // 🟡 All the next transactions will be sent by the deployer if `--broadcast`option is used on the command line 🟡 vm.startBroadcast(deployer); From 0c6ac01405c5b68dc62f446fc778a447c6105e71 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 16:24:06 +0200 Subject: [PATCH 11/31] chore: add a Makefile. --- Makefile | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e529667 --- /dev/null +++ b/Makefile @@ -0,0 +1,45 @@ +-include .env + +.EXPORT_ALL_VARIABLES: +MAKEFLAGS += --no-print-directory + +default: + forge fmt && forge build + +# Always keep Forge up to date +install: + foundryup + forge install + +# Gas +snapshot: + @forge snapshot + +# Tests +test: + @forge test --summary + +test-f-%: + @FOUNDRY_MATCH_TEST=$* make test + +test-c-%: + @FOUNDRY_MATCH_CONTRACT=$* make test + +# Coverage +coverage: + @forge coverage --report lcov + @lcov --ignore-errors unused --remove ./lcov.info -o ./lcov.info.pruned "test/*" "script/*" + +coverage-html: + @make coverage + @genhtml ./lcov.info.pruned -o report --branch-coverage --output-dir ./coverage + +# Running scripts +simulate-%: + @forge script script/$*.s.sol --fork-url $(PROVIDER_URL) -vvvvv + +deploy-%: + @forge script script/$*.s.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvvv + +# Override default `test` and `coverage` targets +.PHONY: test coverage \ No newline at end of file From 2dcbae8a06fc348c2138bea4bd6cc2f5340532b9 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 16:26:57 +0200 Subject: [PATCH 12/31] chore: add `.env` example. --- .env.example | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b1fd9fe --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +### Providers URL ### +PROVIDER_URL= # [MANDATORY] : URL Provider for mainnet forks. + +### Etherscan ### +# ETHERSCAN_API_KEY= # [OPTIONAL] : API Key for Etherscan. Useful for verifying contracts and reading logs on forks. + +### Deployer ### +# DEPLOYER_PRIVATE_KEY= # [OPTIONAL] : Private key of the deployer. Mandatory only for deploying contracts. \ No newline at end of file From 7662f58c61a7c39ff368d5c854992712e9ab6a52 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 23:29:13 +0200 Subject: [PATCH 13/31] [WIP]: add PoC for tasks in Solidity. --- script/Swap.s.sol | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 script/Swap.s.sol diff --git a/script/Swap.s.sol b/script/Swap.s.sol new file mode 100644 index 0000000..9bb5e36 --- /dev/null +++ b/script/Swap.s.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.sol"; +import {Script} from "forge-std/Script.sol"; + +import {OethARM} from "contracts/OethARM.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +contract SwapScript is Script { + address public deployer; + + bytes32 emptyStringHash = keccak256(abi.encodePacked("")); + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + + function setUp() public { + if (vm.envExists("DEPLOYER_PRIVATE_KEY")) { + console.log("Deployer private key found in env"); + // Fetch PK from env and derive the deployer address + deployer = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); + } else { + // If no PK is provided, use a default deployer address + deployer = makeAddr("deployer"); + } + } + + ////////////////////////////////////////////////////// + /// --- TASKS + ////////////////////////////////////////////////////// + function swap(address from, address to, uint256 amount) public { + vm.startBroadcast(deployer); + + if (from != address(0) && to != address(0)) { + revert("Cannot specify both from and to asset. It has to be one or the other"); + } + + if (from != address(0)) { + require(from == Mainnet.OETH || from == Mainnet.WETH, "Invalid from asset"); + + to = from == Mainnet.OETH ? Mainnet.WETH : Mainnet.OETH; + + string memory message = string( + abi.encodePacked( + "About to swap ", + vm.toString(amount), + " ", + vm.toString(from), + " to ", + vm.toString(to), + " for ", + vm.toString(deployer) + ) + ); + + console.log(message); + //OethARM(Mainnet.) + } + + vm.stopBroadcast(); + } +} From 6e8b01a9f2db72eee114aa763889e763ecd17cc1 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Fri, 19 Jul 2024 23:41:18 +0200 Subject: [PATCH 14/31] fix import --- script/Swap.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/Swap.s.sol b/script/Swap.s.sol index 9bb5e36..d4e413d 100644 --- a/script/Swap.s.sol +++ b/script/Swap.s.sol @@ -5,7 +5,7 @@ pragma solidity 0.8.23; import {console} from "forge-std/console.sol"; import {Script} from "forge-std/Script.sol"; -import {OethARM} from "contracts/OethARM.sol"; +import {OEthARM} from "contracts/OethARM.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; From 518fbde420c092c282bd2d92b03fc625d38a72ce Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 21 Jul 2024 00:30:51 +0200 Subject: [PATCH 15/31] chore: add all broadcast files to `.gitignore`. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 84e39b7..6ea6dbf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ bin/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +broadcast logs # Dotenv file From b011a4cb16031040532cc24b30f055b83e1104ff Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 21 Jul 2024 00:32:02 +0200 Subject: [PATCH 16/31] fix: swap task. --- script/{Swap.s.sol => 999_Tasks.s.sol} | 36 ++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) rename script/{Swap.s.sol => 999_Tasks.s.sol} (63%) diff --git a/script/Swap.s.sol b/script/999_Tasks.s.sol similarity index 63% rename from script/Swap.s.sol rename to script/999_Tasks.s.sol index d4e413d..d885df8 100644 --- a/script/Swap.s.sol +++ b/script/999_Tasks.s.sol @@ -5,12 +5,16 @@ pragma solidity 0.8.23; import {console} from "forge-std/console.sol"; import {Script} from "forge-std/Script.sol"; +// Contracts import {OEthARM} from "contracts/OethARM.sol"; +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + // Utils import {Mainnet} from "test/utils/Addresses.sol"; -contract SwapScript is Script { +contract _999_TasksScript is Script { address public deployer; bytes32 emptyStringHash = keccak256(abi.encodePacked("")); @@ -58,7 +62,35 @@ contract SwapScript is Script { ); console.log(message); - //OethARM(Mainnet.) + + // Execute the swap + OEthARM(Mainnet.OETHARM).swapExactTokensForTokens(IERC20(from), IERC20(to), amount, 0, deployer); + } else if (to != address(0)) { + require(to == Mainnet.OETH || to == Mainnet.WETH, "Invalid to asset"); + + from = to == Mainnet.OETH ? Mainnet.WETH : Mainnet.OETH; + + string memory message = string( + abi.encodePacked( + "About to swap ", + vm.toString(from), + " to ", + vm.toString(amount), + " ", + vm.toString(to), + " for ", + vm.toString(deployer) + ) + ); + + console.log(message); + + // Execute the swap + OEthARM(Mainnet.OETHARM).swapTokensForExactTokens( + IERC20(from), IERC20(to), amount, type(uint256).max, deployer + ); + } else { + revert("Must specify either from or to asset"); } vm.stopBroadcast(); From b6f4df698a135689d7b0e491454b5b9ca7d6de1f Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 21 Jul 2024 00:32:29 +0200 Subject: [PATCH 17/31] fix: improve deployment file. --- script/001_OETH_ARM.s.sol | 5 ++++- test/utils/Addresses.sol | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/script/001_OETH_ARM.s.sol b/script/001_OETH_ARM.s.sol index 927cc8d..b02703f 100644 --- a/script/001_OETH_ARM.s.sol +++ b/script/001_OETH_ARM.s.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.23; // Foundry +import {console} from "forge-std/console.sol"; import {Script} from "forge-std/Script.sol"; // Contracts @@ -21,11 +22,13 @@ contract _001_OETHARMScript is Script { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// - function setUp() public { + function setUp() public virtual{ if (vm.envExists("DEPLOYER_PRIVATE_KEY")) { + console.log("Using real deployer address"); // Fetch PK from env and derive the deployer address deployer = vm.addr(vm.envUint("DEPLOYER_PRIVATE_KEY")); } else { + console.log("Using default deployer address"); // If no PK is provided, use a default deployer address deployer = makeAddr("deployer"); } diff --git a/test/utils/Addresses.sol b/test/utils/Addresses.sol index 57c1d87..2cafefc 100644 --- a/test/utils/Addresses.sol +++ b/test/utils/Addresses.sol @@ -16,4 +16,5 @@ library Mainnet { address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address public constant TIMELOCK = 0x35918cDE7233F2dD33fA41ae3Cb6aE0e42E0e69F; address public constant OETHVAULT = 0x39254033945AA2E4809Cc2977E7087BEE48bd7Ab; + address public constant OETHARM = 0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b; } From 7ed9fda4e4ff18e995332f9d9dfeb5bf32432c88 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 21 Jul 2024 00:33:06 +0200 Subject: [PATCH 18/31] feat: add new rules for deployment and task. --- Makefile | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e529667..24cad06 100644 --- a/Makefile +++ b/Makefile @@ -34,12 +34,21 @@ coverage-html: @make coverage @genhtml ./lcov.info.pruned -o report --branch-coverage --output-dir ./coverage -# Running scripts -simulate-%: +# Deploy contract +simulate-c-%: @forge script script/$*.s.sol --fork-url $(PROVIDER_URL) -vvvvv -deploy-%: +deploy-c-%: @forge script script/$*.s.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvvv +# Tasks +simulate-t-swap: + forge clean + forge script script/999_Tasks.s.sol --fork-url $(PROVIDER_URL) -vvvvv -s "swap(address,address,uint256)" $(FROM) $(TO) $(AMOUNT) + +run-t-swap: + forge clean + forge script script/999_Tasks.s.sol --rpc-url $(PROVIDER_URL) --private-key ${DEPLOYER_PRIVATE_KEY} --broadcast --slow --verify -vvvvv -s "swap(address,address,uint256)" $(FROM) $(TO) $(AMOUNT) + # Override default `test` and `coverage` targets .PHONY: test coverage \ No newline at end of file From f049fe4ad491fe3b0e10560bac3ae1df278a7407 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Sun, 21 Jul 2024 00:35:17 +0200 Subject: [PATCH 19/31] apply fmt --- script/001_OETH_ARM.s.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/001_OETH_ARM.s.sol b/script/001_OETH_ARM.s.sol index b02703f..4f1bbc7 100644 --- a/script/001_OETH_ARM.s.sol +++ b/script/001_OETH_ARM.s.sol @@ -22,7 +22,7 @@ contract _001_OETHARMScript is Script { ////////////////////////////////////////////////////// /// --- SETUP ////////////////////////////////////////////////////// - function setUp() public virtual{ + function setUp() public virtual { if (vm.envExists("DEPLOYER_PRIVATE_KEY")) { console.log("Using real deployer address"); // Fetch PK from env and derive the deployer address From 381f2dbe6d064464769452bc36024a164a26c5ba Mon Sep 17 00:00:00 2001 From: clement-ux Date: Mon, 22 Jul 2024 16:21:56 +0200 Subject: [PATCH 20/31] docs: add description to contracts pupose. --- test/Base.sol | 7 +++++++ test/fork/concrete/Ownable.t.sol | 1 + test/fork/concrete/Proxy.t.sol | 1 + test/fork/concrete/SwapExactTokensForTokens.t.sol | 1 + test/fork/concrete/SwapTokensForExactTokens.t.sol | 1 + test/fork/concrete/Transfer.t.sol | 1 + test/fork/concrete/Withdraw.t.sol | 2 ++ test/fork/shared/Shared.sol | 14 ++++++++++++++ 8 files changed, 28 insertions(+) diff --git a/test/Base.sol b/test/Base.sol index 478e029..e6f9620 100644 --- a/test/Base.sol +++ b/test/Base.sol @@ -12,6 +12,13 @@ import {OEthARM} from "contracts/OethARM.sol"; import {IERC20} from "contracts/Interfaces.sol"; import {IOETHVault} from "contracts/Interfaces.sol"; +/// @notice This contract should be the common parent for all test contracts. +/// It should be used to define common variables and that will be +/// used across all test contracts. This pattern is used to allow different +/// test contracts to share common variables, and ensure a consistent setup. +/// @dev This contract should be inherited by "Shared" contracts. +/// @dev This contract should only be used as storage for common variables. +/// @dev Helpers and other functions should be defined in a separate contract. abstract contract Base_Test_ is Test { ////////////////////////////////////////////////////// /// --- CONTRACTS diff --git a/test/fork/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol index a44e064..63bbb1b 100644 --- a/test/fork/concrete/Ownable.t.sol +++ b/test/fork/concrete/Ownable.t.sol @@ -7,6 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; +/// @notice The puprose of this contract is to test the `Ownable` contract. contract Fork_Concrete_OethARM_Ownable_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- REVERTING TESTS diff --git a/test/fork/concrete/Proxy.t.sol b/test/fork/concrete/Proxy.t.sol index e643fff..30082b4 100644 --- a/test/fork/concrete/Proxy.t.sol +++ b/test/fork/concrete/Proxy.t.sol @@ -10,6 +10,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; +/// @notice The puprose of this contract is to test the `Proxy` contract. contract Fork_Concrete_OethARM_Proxy_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- REVERTING TESTS diff --git a/test/fork/concrete/SwapExactTokensForTokens.t.sol b/test/fork/concrete/SwapExactTokensForTokens.t.sol index 0cb6a52..d9124e2 100644 --- a/test/fork/concrete/SwapExactTokensForTokens.t.sol +++ b/test/fork/concrete/SwapExactTokensForTokens.t.sol @@ -7,6 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; +/// @notice The puprose of this contract is to test the `swapExactTokensForTokens` function in the `OEthARM` contract. contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP diff --git a/test/fork/concrete/SwapTokensForExactTokens.t.sol b/test/fork/concrete/SwapTokensForExactTokens.t.sol index 04d46fc..89c8804 100644 --- a/test/fork/concrete/SwapTokensForExactTokens.t.sol +++ b/test/fork/concrete/SwapTokensForExactTokens.t.sol @@ -7,6 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; +/// @notice The puprose of this contract is to test the `swapTokensForExactTokens` function in the `OEthARM` contract. contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP diff --git a/test/fork/concrete/Transfer.t.sol b/test/fork/concrete/Transfer.t.sol index 248b364..218ba92 100644 --- a/test/fork/concrete/Transfer.t.sol +++ b/test/fork/concrete/Transfer.t.sol @@ -7,6 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; +/// @notice The puprose of this contract is to test the `transferToken` and `transferEth` functions in the `OEthARM` contract. contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { bool public shoudRevertOnReceive; diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index 9950a74..4f155e9 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -7,6 +7,8 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; +/// @notice The puprose of this contract is to test the `requestWithdrawal`, +/// `claimWithdrawal` and `claimWithdrawals` functions in the `OEthARM` contract. contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 62c36f9..6adffdf 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -15,6 +15,20 @@ import {IOETHVault} from "contracts/Interfaces.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; +/// @notice This contract should inherit from `Base_Test_`. It should be used to setup the FORK test ONLY! +/// @dev This contract will be used to: +/// - Create and select a fork. +/// - Create users (generating addresses). +/// - Deploy contracts in the fork for testing. +/// - Label contracts for easy identification. +/// - Apply post deployment setup if needed. +/// @dev This contract can inherit from other `Helpers` contracts to add more functionality like: +/// - Modifiers used often in tests. +/// - Extra assertions (like to compare unsual types). +/// - Maths helpers. +/// - etc. +/// @dev This contract should be inherited by `Concrete` and `Fuzz` test contracts. +/// @dev `setUp()` function should be marked as `virtual` to allow overriding in child contracts. abstract contract Fork_Shared_Test_ is Base_Test_ { uint256 public forkId; From 97c0d28cfd35f64026edafbfc8398eafecbc94d5 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Mon, 22 Jul 2024 17:04:38 +0200 Subject: [PATCH 21/31] feat: implement asOwner modifier. --- test/fork/concrete/Ownable.t.sol | 6 ++---- test/fork/concrete/Proxy.t.sol | 6 ++---- test/fork/concrete/Transfer.t.sol | 9 +++------ test/fork/concrete/Withdraw.t.sol | 12 +++--------- test/fork/utils/Modifiers.sol | 21 +++++++++++++++++++++ 5 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 test/fork/utils/Modifiers.sol diff --git a/test/fork/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol index 63bbb1b..41fc695 100644 --- a/test/fork/concrete/Ownable.t.sol +++ b/test/fork/concrete/Ownable.t.sol @@ -27,22 +27,20 @@ contract Fork_Concrete_OethARM_Ownable_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_SetOperator() public { + function test_SetOperator() public asOwner { // Assertions before assertEq(oethARM.operator(), address(0)); - vm.prank(oethARM.owner()); oethARM.setOperator(operator); // Assertions after assertEq(oethARM.operator(), operator); } - function test_SetOwner() public { + function test_SetOwner() public asOwner { // Assertions before assertEq(oethARM.owner(), Mainnet.TIMELOCK); - vm.prank(oethARM.owner()); oethARM.setOwner(alice); // Assertions after diff --git a/test/fork/concrete/Proxy.t.sol b/test/fork/concrete/Proxy.t.sol index 30082b4..88f1599 100644 --- a/test/fork/concrete/Proxy.t.sol +++ b/test/fork/concrete/Proxy.t.sol @@ -32,12 +32,11 @@ contract Fork_Concrete_OethARM_Proxy_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_Upgrade() public { + function test_Upgrade() public asOwner { address owner = Mainnet.TIMELOCK; // Deploy new implementation OEthARM newImplementation = new OEthARM(); - vm.prank(owner); proxy.upgradeTo(address(newImplementation)); assertEq(proxy.implementation(), address(newImplementation)); @@ -50,14 +49,13 @@ contract Fork_Concrete_OethARM_Proxy_Test_ is Fork_Shared_Test_ { assertEq(address(oethARM.token1()), Mainnet.WETH); } - function test_UpgradeAndCall() public { + function test_UpgradeAndCall() public asOwner { address owner = Mainnet.TIMELOCK; // Deploy new implementation OEthARM newImplementation = new OEthARM(); bytes memory data = abi.encodeWithSignature("setOperator(address)", address(0x123)); - vm.prank(owner); proxy.upgradeToAndCall(address(newImplementation), data); assertEq(proxy.implementation(), address(newImplementation)); diff --git a/test/fork/concrete/Transfer.t.sol b/test/fork/concrete/Transfer.t.sol index 218ba92..aa3c124 100644 --- a/test/fork/concrete/Transfer.t.sol +++ b/test/fork/concrete/Transfer.t.sol @@ -36,10 +36,9 @@ contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { oethARM.transferEth(address(0), 0); } - function test_RevertWhen_TransferETH_Because_ETHTransferFailed() public { + function test_RevertWhen_TransferETH_Because_ETHTransferFailed() public asOwner { shoudRevertOnReceive = true; - vm.prank(oethARM.owner()); vm.expectRevert("ARM: ETH transfer failed"); oethARM.transferEth(address(this), 10 ether); } @@ -47,14 +46,13 @@ contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_TransferToken() public { + function test_TransferToken() public asOwner { // Assertions before assertEq(weth.balanceOf(address(this)), 0); assertEq(weth.balanceOf(address(oethARM)), 100 ether); vm.expectEmit({emitter: address(weth)}); emit IERC20.Transfer(address(oethARM), address(this), 10 ether); - vm.prank(oethARM.owner()); oethARM.transferToken(address(weth), address(this), 10 ether); // Assertions after @@ -62,12 +60,11 @@ contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(oethARM)), 90 ether); } - function test_TransferETH() public { + function test_TransferETH() public asOwner { // Assertions before uint256 balanceBefore = address(this).balance; assertEq(address(oethARM).balance, 100 ether); - vm.prank(oethARM.owner()); oethARM.transferEth(address(this), 10 ether); // Assertions after diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index 4f155e9..92d222d 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -49,8 +49,7 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_RequestWithdraw() public { - vm.prank(oethARM.owner()); + function test_RequestWithdraw() public asOwner { vm.expectEmit({emitter: address(oeth)}); emit IERC20.Transfer(address(oethARM), address(0), 1 ether); (uint256 requestId, uint256 queued) = oethARM.requestWithdrawal(1 ether); @@ -61,28 +60,24 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { assertEq(oeth.balanceOf(address(oethARM)), 9 ether, "OETH balance should be 99 ether"); } - function test_ClaimWithdraw_() public { + function test_ClaimWithdraw_() public asOwner { // First request withdrawal - vm.prank(oethARM.owner()); (uint256 requestId,) = oethARM.requestWithdrawal(1 ether); vault.addWithdrawalQueueLiquidity(); skip(10 minutes); // Todo: fetch direct value from contract // Then claim withdrawal - vm.prank(oethARM.owner()); oethARM.claimWithdrawal(requestId); // Assertions after assertEq(weth.balanceOf(address(oethARM)), 1 ether, "WETH balance should be 1 ether"); } - function test_ClaimWithdraws() public { + function test_ClaimWithdraws() public asOwner { // First request withdrawal - vm.startPrank(oethARM.owner()); oethARM.requestWithdrawal(1 ether); oethARM.requestWithdrawal(1 ether); - vm.stopPrank(); vault.addWithdrawalQueueLiquidity(); skip(10 minutes); // Todo: fetch direct value from contract @@ -91,7 +86,6 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { requestIds[0] = 0; requestIds[1] = 1; // Then claim withdrawal - vm.prank(oethARM.owner()); oethARM.claimWithdrawals(requestIds); // Assertions after diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol new file mode 100644 index 0000000..bb65c62 --- /dev/null +++ b/test/fork/utils/Modifiers.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Helpers} from "test/fork/utils/Helpers.sol"; + +abstract contract Modifiers is Helpers { + /// @notice Impersonate the owner of the contract. + modifier asOwner() { + vm.startPrank(oethARM.owner()); + _; + vm.stopPrank(); + } + + /// @notice Impersonate the governor of the vault. + modifier asGovernor() { + vm.startPrank(vault.governor()); + _; + vm.stopPrank(); + } +} From 71a09e6610614b2276d38cf167f45f59a3542fc4 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Mon, 22 Jul 2024 17:06:25 +0200 Subject: [PATCH 22/31] feat: add helper and modifier to Shared tests. --- test/fork/shared/Shared.sol | 22 ++++------------------ test/fork/utils/Helpers.sol | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 test/fork/utils/Helpers.sol diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 6adffdf..90fa8b1 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.23; // Test imports -import {Base_Test_} from "test/Base.sol"; +import {Modifiers} from "test/fork/utils/Modifiers.sol"; // Contracts import {Proxy} from "contracts/Proxy.sol"; @@ -15,7 +15,8 @@ import {IOETHVault} from "contracts/Interfaces.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; -/// @notice This contract should inherit from `Base_Test_`. It should be used to setup the FORK test ONLY! +/// @notice This contract should inherit (directly or indirectly) from `Base_Test_`. +/// It should be used to setup the FORK test ONLY! /// @dev This contract will be used to: /// - Create and select a fork. /// - Create users (generating addresses). @@ -29,7 +30,7 @@ import {Mainnet} from "test/utils/Addresses.sol"; /// - etc. /// @dev This contract should be inherited by `Concrete` and `Fuzz` test contracts. /// @dev `setUp()` function should be marked as `virtual` to allow overriding in child contracts. -abstract contract Fork_Shared_Test_ is Base_Test_ { +abstract contract Fork_Shared_Test_ is Modifiers { uint256 public forkId; ////////////////////////////////////////////////////// @@ -100,19 +101,4 @@ abstract contract Fork_Shared_Test_ is Base_Test_ { vm.label(Mainnet.TIMELOCK, "TIMELOCK"); vm.label(Mainnet.NULL, "NULL"); } - - /// @notice Override `deal()` function to handle OETH special case. - function deal(address token, address to, uint256 amount) internal override { - // Handle OETH special case, as rebasing tokens are not supported by the VM. - if (token == address(oeth)) { - // Check than whale as enough OETH. - require(oeth.balanceOf(Mainnet.WHALE_OETH) >= amount, "Fork_Shared_Test_: Not enough OETH in WHALE_OETH"); - - // Transfer OETH from WHALE_OETH to the user. - vm.prank(Mainnet.WHALE_OETH); - oeth.transfer(to, amount); - } else { - super.deal(token, to, amount); - } - } } diff --git a/test/fork/utils/Helpers.sol b/test/fork/utils/Helpers.sol new file mode 100644 index 0000000..9306411 --- /dev/null +++ b/test/fork/utils/Helpers.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Base_Test_} from "test/Base.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +abstract contract Helpers is Base_Test_ { + /// @notice Override `deal()` function to handle OETH special case. + function deal(address token, address to, uint256 amount) internal override { + // Handle OETH special case, as rebasing tokens are not supported by the VM. + if (token == address(oeth)) { + // Check than whale as enough OETH. + require(oeth.balanceOf(Mainnet.WHALE_OETH) >= amount, "Fork_Shared_Test_: Not enough OETH in WHALE_OETH"); + + // Transfer OETH from WHALE_OETH to the user. + vm.prank(Mainnet.WHALE_OETH); + oeth.transfer(to, amount); + } else { + super.deal(token, to, amount); + } + } +} From bec97029a0dd55ba0708c9354fe955494d488ad6 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Mon, 22 Jul 2024 17:17:53 +0200 Subject: [PATCH 23/31] feat: add mockCall library. --- test/fork/concrete/Withdraw.t.sol | 9 +++------ test/fork/shared/Shared.sol | 2 +- test/fork/utils/MockCall.sol | 14 ++++++++++++++ test/fork/utils/Modifiers.sol | 7 +++++++ 4 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 test/fork/utils/MockCall.sol diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index 92d222d..a0d8086 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -23,9 +23,6 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { // Remove solvency check vm.prank(vault.governor()); vault.setMaxSupplyDiff(0); - - // Bypass call to the dripper - vm.mockCall({callee: vault.dripper(), data: abi.encodeWithSignature("collect()"), returnData: abi.encode(true)}); } ////////////////////////////////////////////////////// @@ -49,7 +46,7 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- PASSING TESTS ////////////////////////////////////////////////////// - function test_RequestWithdraw() public asOwner { + function test_RequestWithdraw() public asOwner mockCallDripperCollect { vm.expectEmit({emitter: address(oeth)}); emit IERC20.Transfer(address(oethARM), address(0), 1 ether); (uint256 requestId, uint256 queued) = oethARM.requestWithdrawal(1 ether); @@ -60,7 +57,7 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { assertEq(oeth.balanceOf(address(oethARM)), 9 ether, "OETH balance should be 99 ether"); } - function test_ClaimWithdraw_() public asOwner { + function test_ClaimWithdraw_() public asOwner mockCallDripperCollect { // First request withdrawal (uint256 requestId,) = oethARM.requestWithdrawal(1 ether); @@ -74,7 +71,7 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { assertEq(weth.balanceOf(address(oethARM)), 1 ether, "WETH balance should be 1 ether"); } - function test_ClaimWithdraws() public asOwner { + function test_ClaimWithdraws() public asOwner mockCallDripperCollect { // First request withdrawal oethARM.requestWithdrawal(1 ether); oethARM.requestWithdrawal(1 ether); diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 90fa8b1..38e93d2 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -15,7 +15,7 @@ import {IOETHVault} from "contracts/Interfaces.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; -/// @notice This contract should inherit (directly or indirectly) from `Base_Test_`. +/// @notice This contract should inherit (directly or indirectly) from `Base_Test_`. /// It should be used to setup the FORK test ONLY! /// @dev This contract will be used to: /// - Create and select a fork. diff --git a/test/fork/utils/MockCall.sol b/test/fork/utils/MockCall.sol new file mode 100644 index 0000000..56bd606 --- /dev/null +++ b/test/fork/utils/MockCall.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {Vm} from "forge-std/Vm.sol"; + +/// @notice This contract should be used to mock calls to other contracts. +library MockCall { + Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function mockCallDripperCollect(address dripper) external { + vm.mockCall({callee: dripper, data: abi.encodeWithSignature("collect()"), returnData: abi.encode(true)}); + } +} diff --git a/test/fork/utils/Modifiers.sol b/test/fork/utils/Modifiers.sol index bb65c62..acc8825 100644 --- a/test/fork/utils/Modifiers.sol +++ b/test/fork/utils/Modifiers.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.23; // Test imports import {Helpers} from "test/fork/utils/Helpers.sol"; +import {MockCall} from "test/fork/utils/MockCall.sol"; abstract contract Modifiers is Helpers { /// @notice Impersonate the owner of the contract. @@ -18,4 +19,10 @@ abstract contract Modifiers is Helpers { _; vm.stopPrank(); } + + /// @notice Mock the call to the dripper's `collect` function, bypass it and return `true`. + modifier mockCallDripperCollect() { + MockCall.mockCallDripperCollect(vault.dripper()); + _; + } } From 41cb3705e8d0ea4a5eeae1f1563eb31bd0500306 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment?= <55331875+clement-ux@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:47:42 +0200 Subject: [PATCH 24/31] feat: update readme. --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 00e1ac7..b3c09b6 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,26 @@ forge compile ``` ### Running tests +Fork and Unit tests run with the same command, as the fork is initiated on the test file itself if needed. +By default: +- verbosity is set to 3, i.e. are displayed logs, assertion errors (expected vs actual), and stack traces for failing tests. +- a summary of the test is displayed at the end. + +To run all tests: +``` +make test +``` + +To run only a test contract: +``` +make test-c-TestContractName +``` + +To run only a specific test +``` +make test-f-TestFunctionName +``` ### Running gas report scripts @@ -55,14 +74,38 @@ The following will upload the different Autotask bundles to Defender. `rollup` and `defender-autotask` can be installed globally to avoid the `npx` prefix. -## Deployment +## Script -### Dry-run on fork +### Testing script +- The deployment will happen on RPC used on the .env file, under `PROVIDER_URL`. +- If `DEPLOYER_PRIVATE_KEY` key exist, it will use it to simulate the deployment. +- Otherwise it will create an address for the test. +#### For smart contract +``` +make simulate-c-ScriptContractName +# example: make simulate-c-001_OETH_ARM ``` -$ DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY forge script script/deploy/DeployManager.sol:DeployManager --fork-url $ALCHEMY_PROVIDER_URL +#### For task +``` +make simulate-t-taskName $(ARGS1) $(ARGS2) +# example: make simulate-task-swap FROM=0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3 TO=0x0000000000000000000000000000000000000000 AMOUNT=1234 ``` -### Deployment with Verification +### Running script +The `DEPLOYER_PRIVATE_KEY` on the `.env` is mandatory here! +It will run with the following options: +- broadcast (send transaction for real) +- slow (i.e. send tx after prior confirmed and succeeded) +- verify (verify contract on Etherscan) +- max verbosity +#### For smart contract +`ETHERSCAN_API_KEY` is mandatory here! +``` +make deploy-c-ScriptContractName +``` + +#### For task +``` +make run-t-taskName $(ARGS1) $(ARGS2) +# example: make run-task-swap FROM=0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3 TO=0x0000000000000000000000000000000000000000 AMOUNT=1234 ``` -$ DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY forge script script/deploy/DeployManager.sol:DeployManager --fork-url $ALCHEMY_PROVIDER_URL --slow --legacy --broadcast --verify --etherscan-api-key $ETHERSCAN_API_KEY -``` \ No newline at end of file From 9939959329ebf1012134989c4edb3d1a7dc680d4 Mon Sep 17 00:00:00 2001 From: clement-ux Date: Mon, 29 Jul 2024 23:02:40 +0200 Subject: [PATCH 25/31] test: match test with current deployed contract on virtual testnet. --- src/contracts/Interfaces.sol | 6 ++++++ test/fork/concrete/Withdraw.t.sol | 25 +++++++++++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 5a5ad53..bd79ddd 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -56,6 +56,12 @@ interface IOETHVault { function governor() external returns (address); function dripper() external returns (address); + + function withdrawalQueueMetadata() + external + returns (uint128 queued, uint128 claimable, uint128 claimed, uint128 nextWithdrawalIndex); + + function CLAIM_DELAY() external returns (uint256); } interface IGovernance { diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index a0d8086..cebe5b7 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -47,13 +47,14 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { /// --- PASSING TESTS ////////////////////////////////////////////////////// function test_RequestWithdraw() public asOwner mockCallDripperCollect { + (uint128 queuedBefore,,, uint128 nextWithdrawalIndex) = vault.withdrawalQueueMetadata(); vm.expectEmit({emitter: address(oeth)}); emit IERC20.Transfer(address(oethARM), address(0), 1 ether); (uint256 requestId, uint256 queued) = oethARM.requestWithdrawal(1 ether); // Assertions after - assertEq(requestId, 0, "Request ID should be 0"); - assertEq(queued, 1 ether, "Queued amount should be 1 ether"); + assertEq(requestId, nextWithdrawalIndex, "Request ID should be 0"); + assertEq(queued, queuedBefore + 1 ether, "Queued amount should be 1 ether"); assertEq(oeth.balanceOf(address(oethARM)), 9 ether, "OETH balance should be 99 ether"); } @@ -61,7 +62,14 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { // First request withdrawal (uint256 requestId,) = oethARM.requestWithdrawal(1 ether); + // Add more liquidity to facilitate withdrawal + (, uint128 claimable, uint128 claimed,) = vault.withdrawalQueueMetadata(); + deal(address(weth), address(vault), claimable - claimed + 1 ether); + + // Add liquidity to the withdrawal queue vault.addWithdrawalQueueLiquidity(); + + // Skip delay skip(10 minutes); // Todo: fetch direct value from contract // Then claim withdrawal @@ -72,16 +80,25 @@ contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { } function test_ClaimWithdraws() public asOwner mockCallDripperCollect { + (,,, uint128 nextWithdrawalIndex) = vault.withdrawalQueueMetadata(); + // First request withdrawal oethARM.requestWithdrawal(1 ether); oethARM.requestWithdrawal(1 ether); + // Add more liquidity to facilitate withdrawal + (, uint128 claimable, uint128 claimed,) = vault.withdrawalQueueMetadata(); + deal(address(weth), address(vault), claimable - claimed + 1 ether); + + // Add liquidity to the withdrawal queue vault.addWithdrawalQueueLiquidity(); + + // Skip delay skip(10 minutes); // Todo: fetch direct value from contract uint256[] memory requestIds = new uint256[](2); - requestIds[0] = 0; - requestIds[1] = 1; + requestIds[0] = nextWithdrawalIndex; + requestIds[1] = nextWithdrawalIndex + 1; // Then claim withdrawal oethARM.claimWithdrawals(requestIds); From 718935183ea6e097c24bb184b3e5942a6233c956 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 18:40:07 +1000 Subject: [PATCH 26/31] Prettier README --- README.md | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3c09b6..66358e4 100644 --- a/README.md +++ b/README.md @@ -13,23 +13,28 @@ forge compile ``` ### Running tests + Fork and Unit tests run with the same command, as the fork is initiated on the test file itself if needed. -By default: +By default: + - verbosity is set to 3, i.e. are displayed logs, assertion errors (expected vs actual), and stack traces for failing tests. -- a summary of the test is displayed at the end. +- a summary of the test is displayed at the end. To run all tests: + ``` make test ``` To run only a test contract: + ``` make test-c-TestContractName ``` To run only a specific test + ``` make test-f-TestFunctionName ``` @@ -40,7 +45,6 @@ make test-f-TestFunctionName [Open Zeppelin Defender v2](https://docs.openzeppelin.com/defender/v2/) is used to manage the Operations account and automate AMM operational jobs like managing liquidity. - ### Deploying Defender Autotasks Autotasks are used to run operational jobs are specific times or intervals. @@ -73,38 +77,48 @@ The following will upload the different Autotask bundles to Defender. `rollup` and `defender-autotask` can be installed globally to avoid the `npx` prefix. - ## Script ### Testing script + - The deployment will happen on RPC used on the .env file, under `PROVIDER_URL`. - If `DEPLOYER_PRIVATE_KEY` key exist, it will use it to simulate the deployment. - Otherwise it will create an address for the test. + #### For smart contract + ``` make simulate-c-ScriptContractName # example: make simulate-c-001_OETH_ARM ``` + #### For task + ``` make simulate-t-taskName $(ARGS1) $(ARGS2) # example: make simulate-task-swap FROM=0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3 TO=0x0000000000000000000000000000000000000000 AMOUNT=1234 ``` ### Running script + The `DEPLOYER_PRIVATE_KEY` on the `.env` is mandatory here! It will run with the following options: + - broadcast (send transaction for real) - slow (i.e. send tx after prior confirmed and succeeded) - verify (verify contract on Etherscan) - max verbosity + #### For smart contract + `ETHERSCAN_API_KEY` is mandatory here! + ``` make deploy-c-ScriptContractName ``` #### For task + ``` make run-t-taskName $(ARGS1) $(ARGS2) # example: make run-task-swap FROM=0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3 TO=0x0000000000000000000000000000000000000000 AMOUNT=1234 From cba43e75fe304d1556a4ff42e121ad989b5d661b Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 19:00:15 +1000 Subject: [PATCH 27/31] Fix typos --- test/fork/concrete/Ownable.t.sol | 2 +- test/fork/concrete/Proxy.t.sol | 2 +- test/fork/concrete/SwapExactTokensForTokens.t.sol | 8 ++++---- test/fork/concrete/SwapTokensForExactTokens.t.sol | 10 +++++----- test/fork/concrete/Transfer.t.sol | 8 ++++---- test/fork/concrete/Withdraw.t.sol | 2 +- test/fork/shared/Shared.sol | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/fork/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol index 41fc695..324e9ac 100644 --- a/test/fork/concrete/Ownable.t.sol +++ b/test/fork/concrete/Ownable.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; -/// @notice The puprose of this contract is to test the `Ownable` contract. +/// @notice The purpose of this contract is to test the `Ownable` contract. contract Fork_Concrete_OethARM_Ownable_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- REVERTING TESTS diff --git a/test/fork/concrete/Proxy.t.sol b/test/fork/concrete/Proxy.t.sol index 88f1599..1809671 100644 --- a/test/fork/concrete/Proxy.t.sol +++ b/test/fork/concrete/Proxy.t.sol @@ -10,7 +10,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Utils import {Mainnet} from "test/utils/Addresses.sol"; -/// @notice The puprose of this contract is to test the `Proxy` contract. +/// @notice The purpose of this contract is to test the `Proxy` contract. contract Fork_Concrete_OethARM_Proxy_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- REVERTING TESTS diff --git a/test/fork/concrete/SwapExactTokensForTokens.t.sol b/test/fork/concrete/SwapExactTokensForTokens.t.sol index d9124e2..36a798e 100644 --- a/test/fork/concrete/SwapExactTokensForTokens.t.sol +++ b/test/fork/concrete/SwapExactTokensForTokens.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; -/// @notice The puprose of this contract is to test the `swapExactTokensForTokens` function in the `OEthARM` contract. +/// @notice The purpose of this contract is to test the `swapExactTokensForTokens` function in the `OEthARM` contract. contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP @@ -28,7 +28,7 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes /// --- REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InsuficientOutputAmount() public { + function test_RevertWhen_SwapExactTokensForTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Insufficient output amount"); oethARM.swapExactTokensForTokens(weth, oeth, 10 ether, 11 ether, address(this)); } @@ -58,7 +58,7 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), 0); } - function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalideSwap_TokenIn() public { + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidSwap_TokenIn() public { address[] memory path = new address[](2); path[0] = address(weth); path[1] = address(weth); @@ -66,7 +66,7 @@ contract Fork_Concrete_OethARM_SwapExactTokensForTokens_Test_ is Fork_Shared_Tes oethARM.swapExactTokensForTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); } - function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalideSwap_TokenOut() public { + function test_RevertWhen_SwapExactTokensForTokens_Complex_Because_InvalidSwap_TokenOut() public { address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(oeth); diff --git a/test/fork/concrete/SwapTokensForExactTokens.t.sol b/test/fork/concrete/SwapTokensForExactTokens.t.sol index 89c8804..8487ada 100644 --- a/test/fork/concrete/SwapTokensForExactTokens.t.sol +++ b/test/fork/concrete/SwapTokensForExactTokens.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; -/// @notice The puprose of this contract is to test the `swapTokensForExactTokens` function in the `OEthARM` contract. +/// @notice The purpose of this contract is to test the `swapTokensForExactTokens` function in the `OEthARM` contract. contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// /// --- SETUP @@ -28,7 +28,7 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes /// --- REVERTING TESTS ////////////////////////////////////////////////////// - function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InsuficientOutputAmount() public { + function test_RevertWhen_SwapTokensForExactTokens_Simple_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Excess input amount"); oethARM.swapTokensForExactTokens(weth, oeth, 10 ether, 9 ether, address(this)); } @@ -43,7 +43,7 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes oethARM.swapTokensForExactTokens(oeth, oeth, 10 ether, 10 ether, address(this)); } - function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InsuficientOutputAmount() public { + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InsufficientOutputAmount() public { vm.expectRevert("ARM: Excess input amount"); oethARM.swapTokensForExactTokens(10 ether, 9 ether, new address[](2), address(this), 0); } @@ -58,7 +58,7 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), 0); } - function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalideSwap_TokenIn() public { + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidSwap_TokenIn() public { address[] memory path = new address[](2); path[0] = address(weth); path[1] = address(weth); @@ -66,7 +66,7 @@ contract Fork_Concrete_OethARM_SwapTokensForExactTokens_Test_ is Fork_Shared_Tes oethARM.swapTokensForExactTokens(10 ether, 10 ether, new address[](2), address(this), block.timestamp + 1000); } - function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalideSwap_TokenOut() public { + function test_RevertWhen_SwapTokensForExactTokens_Complex_Because_InvalidSwap_TokenOut() public { address[] memory path = new address[](2); path[0] = address(oeth); path[1] = address(oeth); diff --git a/test/fork/concrete/Transfer.t.sol b/test/fork/concrete/Transfer.t.sol index aa3c124..bb4e394 100644 --- a/test/fork/concrete/Transfer.t.sol +++ b/test/fork/concrete/Transfer.t.sol @@ -7,9 +7,9 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; -/// @notice The puprose of this contract is to test the `transferToken` and `transferEth` functions in the `OEthARM` contract. +/// @notice The purpose of this contract is to test the `transferToken` and `transferEth` functions in the `OEthARM` contract. contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { - bool public shoudRevertOnReceive; + bool public shouldRevertOnReceive; ////////////////////////////////////////////////////// /// --- SETUP @@ -37,7 +37,7 @@ contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { } function test_RevertWhen_TransferETH_Because_ETHTransferFailed() public asOwner { - shoudRevertOnReceive = true; + shouldRevertOnReceive = true; vm.expectRevert("ARM: ETH transfer failed"); oethARM.transferEth(address(this), 10 ether); @@ -76,6 +76,6 @@ contract Fork_Concrete_OethARM_Transfer_Test_ is Fork_Shared_Test_ { /// --- RECEIVE ////////////////////////////////////////////////////// receive() external payable { - if (shoudRevertOnReceive) revert(); + if (shouldRevertOnReceive) revert(); } } diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol index cebe5b7..45e3382 100644 --- a/test/fork/concrete/Withdraw.t.sol +++ b/test/fork/concrete/Withdraw.t.sol @@ -7,7 +7,7 @@ import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; // Interfaces import {IERC20} from "contracts/Interfaces.sol"; -/// @notice The puprose of this contract is to test the `requestWithdrawal`, +/// @notice The purpose of this contract is to test the `requestWithdrawal`, /// `claimWithdrawal` and `claimWithdrawals` functions in the `OEthARM` contract. contract Fork_Concrete_OethARM_Withdraw_Test_ is Fork_Shared_Test_ { ////////////////////////////////////////////////////// diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 38e93d2..40e565b 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -25,7 +25,7 @@ import {Mainnet} from "test/utils/Addresses.sol"; /// - Apply post deployment setup if needed. /// @dev This contract can inherit from other `Helpers` contracts to add more functionality like: /// - Modifiers used often in tests. -/// - Extra assertions (like to compare unsual types). +/// - Extra assertions (like to compare unusual types). /// - Maths helpers. /// - etc. /// @dev This contract should be inherited by `Concrete` and `Fuzz` test contracts. From 4bb9015e720978cb211c33403b41531c3c06c832 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 19:00:29 +1000 Subject: [PATCH 28/31] Updated README --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 66358e4..e8c9d1d 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,11 @@ Swap OETH for WETH at 1:1 ratio. foundryup forge install forge compile +cp .env.example .env ``` +In the `.env` file, set the environment variables as needed. eg `PROVIDER_URL` for the RPC endpoint. + ### Running tests Fork and Unit tests run with the same command, as the fork is initiated on the test file itself if needed. From 02e9f5fe4bc7b8f79ffba308a7c1d80a9386467f Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 19:10:21 +1000 Subject: [PATCH 29/31] Add gas command for gas usage report --- .gitignore | 3 +++ Makefile | 5 ++++- README.md | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 6ea6dbf..e673bae 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ yarn.lock cache_hardhat artifacts +# Forge +.gas-snapshot + # Defender Actions dist diff --git a/Makefile b/Makefile index 24cad06..d9dddf0 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,10 @@ install: foundryup forge install -# Gas +gas: + @forge test --gas-report + +# Generate gas snapshots for all your test functions snapshot: @forge snapshot diff --git a/README.md b/README.md index e8c9d1d..9f5b11b 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,11 @@ To run only a specific test make test-f-TestFunctionName ``` -### Running gas report scripts +Report gas usage for tests: + +``` +make gas +``` ## Open Zeppelin Defender From 09439b5a43de778c02a7041281d1759acf96a143 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 19:26:41 +1000 Subject: [PATCH 30/31] Restore DeployManager --- script/deploy/DeployManager.sol | 170 ++++++++++++++++++++ script/deploy/mainnet/001_DeployCore.sol | 55 +++++++ script/deploy/mainnet/BaseMainnetScript.sol | 102 ++++++++++++ 3 files changed, 327 insertions(+) create mode 100644 script/deploy/DeployManager.sol create mode 100644 script/deploy/mainnet/001_DeployCore.sol create mode 100644 script/deploy/mainnet/BaseMainnetScript.sol diff --git a/script/deploy/DeployManager.sol b/script/deploy/DeployManager.sol new file mode 100644 index 0000000..9f32d7c --- /dev/null +++ b/script/deploy/DeployManager.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.23; + +import "forge-std/Script.sol"; + +import {BaseMainnetScript} from "./mainnet/BaseMainnetScript.sol"; + +import {DeployCoreScript} from "./mainnet/001_DeployCore.sol"; + +import {VmSafe} from "forge-std/Vm.sol"; + +contract DeployManager is Script { + mapping(string => address) public deployedContracts; + mapping(string => bool) public scriptsExecuted; + + string internal forkFileId = ""; + + bool public isForked; + + constructor() { + isForked = vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); + forkFileId = vm.toString(block.timestamp); + } + + function getDeploymentFilePath() public view returns (string memory) { + return isForked ? getForkDeploymentFilePath() : getMainnetDeploymentFilePath(); + } + + function getMainnetDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments.json")); + } + + function getForkDeploymentFilePath() public view returns (string memory) { + return string(abi.encodePacked(vm.projectRoot(), "/build/deployments-fork", forkFileId, ".json")); + } + + function setForkFileId(string memory _forkFileId) external { + forkFileId = _forkFileId; + } + + function setUp() external { + string memory chainIdStr = vm.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + string memory mainnetFilePath = getMainnetDeploymentFilePath(); + if (!vm.isFile(mainnetFilePath)) { + // Create mainnet deployment file if it doesn't exist + vm.writeFile( + mainnetFilePath, + string(abi.encodePacked('{ "', chainIdStr, '": { "executions": {}, "contracts": {} } }')) + ); + } else if (!vm.keyExistsJson(vm.readFile(mainnetFilePath), chainIdKey)) { + // Create network entry if it doesn't exist + vm.writeJson( + vm.serializeJson(chainIdStr, '{ "executions": {}, "contracts": {} }'), mainnetFilePath, chainIdKey + ); + } + + if (isForked) { + // Duplicate Mainnet File + vm.writeFile(getForkDeploymentFilePath(), vm.readFile(mainnetFilePath)); + } + } + + function run() external { + // TODO: Use vm.readDir to recursively build this? + _runDeployFile(new DeployCoreScript()); + } + + function _runDeployFile(BaseMainnetScript deployScript) internal { + if (deployScript.proposalExecuted()) { + // No action to do + return; + } else if (deployScript.skip()) { + console.log("Skipping deployment (skip() == true)"); + return; + } + + string memory chainIdStr = vm.toString(block.chainid); + string memory chainIdKey = string(abi.encodePacked(".", chainIdStr)); + + string memory contractsKey = string(abi.encodePacked(chainIdKey, ".contracts")); + string memory executionsKey = string(abi.encodePacked(chainIdKey, ".executions")); + + string memory deploymentsFilePath = getDeploymentFilePath(); + string memory fileContents = vm.readFile(deploymentsFilePath); + + /** + * Execution History + */ + string memory currentExecutions = ""; + string[] memory executionKeys = vm.parseJsonKeys(fileContents, executionsKey); + + for (uint256 i = 0; i < executionKeys.length; ++i) { + uint256 deployedTimestamp = + vm.parseJsonUint(fileContents, string(abi.encodePacked(executionsKey, ".", executionKeys[i]))); + + currentExecutions = vm.serializeUint(executionsKey, executionKeys[i], deployedTimestamp); + scriptsExecuted[executionKeys[i]] = true; + } + + /** + * Pre-deployment + */ + string memory networkDeployments = ""; + string[] memory existingContracts = vm.parseJsonKeys(fileContents, contractsKey); + for (uint256 i = 0; i < existingContracts.length; ++i) { + address deployedAddr = + vm.parseJsonAddress(fileContents, string(abi.encodePacked(contractsKey, ".", existingContracts[i]))); + + networkDeployments = vm.serializeAddress(contractsKey, existingContracts[i], deployedAddr); + + deployedContracts[existingContracts[i]] = deployedAddr; + + deployScript.preloadDeployedContract(existingContracts[i], deployedAddr); + } + + if (scriptsExecuted[deployScript.DEPLOY_NAME()]) { + console.log("Skipping deployment (already deployed)"); + + // Governance handling + deployScript.handleGovernanceProposal(); + } else { + // Deployment + deployScript.setUp(); + deployScript.run(); + + /** + * Post-deployment + */ + BaseMainnetScript.DeployRecord[] memory records = deployScript.getAllDeployRecords(); + + for (uint256 i = 0; i < records.length; ++i) { + string memory name = records[i].name; + address addr = records[i].addr; + + console.log(string(abi.encodePacked("> Recorded Deploy of ", name, " at")), addr); + networkDeployments = vm.serializeAddress(contractsKey, name, addr); + deployedContracts[name] = addr; + } + + /** + * Write Execution History + */ + currentExecutions = vm.serializeUint(executionsKey, deployScript.DEPLOY_NAME(), block.timestamp); + + // Write to file instead of using writeJson to avoid "EOF while parsing a value at line 1 column 0" error. + vm.writeFile( + getForkDeploymentFilePath(), + string( + abi.encodePacked( + '{ "', + chainIdStr, + '": { "executions": ', + currentExecutions, + ', "contracts": ', + networkDeployments, + "}}" + ) + ) + ); + + console.log("> Deployment addresses stored and Deploy script execution complete."); + } + } + + function getDeployment(string calldata contractName) external view returns (address) { + return deployedContracts[contractName]; + } +} diff --git a/script/deploy/mainnet/001_DeployCore.sol b/script/deploy/mainnet/001_DeployCore.sol new file mode 100644 index 0000000..521b6f7 --- /dev/null +++ b/script/deploy/mainnet/001_DeployCore.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.23; + +import "./BaseMainnetScript.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; + +import {OEthARM} from "contracts/OethARM.sol"; +import {Proxy} from "contracts/Proxy.sol"; + +import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; + +contract DeployCoreScript is BaseMainnetScript { + using GovSixHelper for GovProposal; + + GovProposal public govProposal; + + string public constant override DEPLOY_NAME = "000_DeployCoreScript"; + bool public constant override proposalExecuted = false; + + constructor() {} + + function _execute() internal override { + console.log("Deploy:"); + console.log("------------"); + + // 1. Deploy proxy contracts + Proxy proxy = new Proxy(); + _recordDeploy("OETH_ARM", address(proxy)); + + // 2. Deploy implementation + OEthARM implementation = new OEthARM(); + _recordDeploy("OETH_ARM_IMPL", address(implementation)); + + // 3. Initialize proxy + proxy.initialize(address(implementation), Addresses.TIMELOCK, ""); + } + + function _buildGovernanceProposal() internal override { + govProposal.setDescription("Setup OETH ARM Contract"); + + // NOTE: This could be done during deploy of proxy. + // But doing this here to test governance flow. + + // Set operator + govProposal.action(deployedContracts["OETH_ARM"], "setOperator(address)", abi.encode(Addresses.STRATEGIST)); + } + + function _fork() internal override { + // Simulate on fork + govProposal.simulate(); + } +} diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol new file mode 100644 index 0000000..d8f0495 --- /dev/null +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +import "forge-std/console.sol"; + +import {Script} from "forge-std/Script.sol"; +import {Vm, VmSafe} from "forge-std/Vm.sol"; + +import {Addresses} from "contracts/utils/Addresses.sol"; +import {GovProposal, GovSixHelper} from "contracts/utils/GovSixHelper.sol"; + +abstract contract BaseMainnetScript is Script { + using GovSixHelper for GovProposal; + + uint256 public deployBlockNum = type(uint256).max; + + // DeployerRecord stuff to be extracted as well + struct DeployRecord { + string name; + address addr; + } + + DeployRecord[] public deploys; + + mapping(string => address) public deployedContracts; + + function _recordDeploy(string memory name, address addr) internal { + deploys.push(DeployRecord({name: name, addr: addr})); + console.log(string(abi.encodePacked("> Deployed ", name, " at")), addr); + deployedContracts[name] = addr; + } + // End DeployRecord + + function getAllDeployRecords() external view returns (DeployRecord[] memory) { + return deploys; + } + + function preloadDeployedContract(string memory name, address addr) external { + deployedContracts[name] = addr; + } + + function isForked() public view returns (bool) { + return vm.isContext(VmSafe.ForgeContext.ScriptDryRun) || vm.isContext(VmSafe.ForgeContext.TestGroup); + } + + function setUp() external {} + + function run() external { + if (block.chainid != 1) { + revert("Not Mainnet"); + } + // Will not execute script if after this block number + if (block.number > deployBlockNum) { + // console.log("Current block %s, script block %s", block.number, deployBlockNum); + return; + } + + if (this.isForked()) { + address impersonator = Addresses.INITIAL_DEPLOYER; + console.log("Running script on mainnet fork impersonating: %s", impersonator); + vm.startPrank(impersonator); + } else { + uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY"); + address deployer = vm.rememberKey(deployerPrivateKey); + vm.startBroadcast(deployer); + console.log("Deploying on mainnet with deployer: %s", deployer); + } + + _execute(); + + if (this.isForked()) { + vm.stopPrank(); + _buildGovernanceProposal(); + _fork(); + } else { + vm.stopBroadcast(); + } + } + + function DEPLOY_NAME() external view virtual returns (string memory); + + function proposalExecuted() external view virtual returns (bool); + + function skip() external view virtual returns (bool) { + return false; + } + + function _execute() internal virtual; + + function _fork() internal virtual; + + function _buildGovernanceProposal() internal virtual {} + + function handleGovernanceProposal() external virtual { + if (this.proposalExecuted()) { + return; + } + + _buildGovernanceProposal(); + _fork(); + } +} From 185b04fcd358a6f0941bd61d43ac4d3030bb3fb6 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 30 Jul 2024 20:57:08 +1000 Subject: [PATCH 31/31] WIP add DeployManager to Shared tests --- .gitignore | 1 + script/deploy/mainnet/BaseMainnetScript.sol | 3 --- test/fork/shared/Shared.sol | 30 ++++++++++++++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index e673bae..3b14d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ artifacts # Forge .gas-snapshot +build/ # Defender Actions dist diff --git a/script/deploy/mainnet/BaseMainnetScript.sol b/script/deploy/mainnet/BaseMainnetScript.sol index d8f0495..a9ff81a 100644 --- a/script/deploy/mainnet/BaseMainnetScript.sol +++ b/script/deploy/mainnet/BaseMainnetScript.sol @@ -46,9 +46,6 @@ abstract contract BaseMainnetScript is Script { function setUp() external {} function run() external { - if (block.chainid != 1) { - revert("Not Mainnet"); - } // Will not execute script if after this block number if (block.number > deployBlockNum) { // console.log("Current block %s, script block %s", block.number, deployBlockNum); diff --git a/test/fork/shared/Shared.sol b/test/fork/shared/Shared.sol index 40e565b..35367f1 100644 --- a/test/fork/shared/Shared.sol +++ b/test/fork/shared/Shared.sol @@ -1,6 +1,9 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.23; +// Deployer +import {DeployManager} from "script/deploy/DeployManager.sol"; + // Test imports import {Modifiers} from "test/fork/utils/Modifiers.sol"; @@ -32,6 +35,7 @@ import {Mainnet} from "test/utils/Addresses.sol"; /// @dev `setUp()` function should be marked as `virtual` to allow overriding in child contracts. abstract contract Fork_Shared_Test_ is Modifiers { uint256 public forkId; + DeployManager public deployManager; ////////////////////////////////////////////////////// /// --- SETUP @@ -39,22 +43,32 @@ abstract contract Fork_Shared_Test_ is Modifiers { function setUp() public virtual override { super.setUp(); - // 1. Create fork. + // 1. Create deploy manager. + // _createDeployManager(); + + // 2. Create fork. _createAndSelectFork(); - // 2. Create users. + // 3. Create users. _generateAddresses(); - // 3. Deploy contracts. + // 4. Deploy contracts. _deployContracts(); - // 4. Label contracts. + // 5. Label contracts. _label(); } ////////////////////////////////////////////////////// /// --- HELPERS ////////////////////////////////////////////////////// + function _createDeployManager() internal { + deployManager = new DeployManager(); + + deployManager.setUp(); + deployManager.run(); + } + function _createAndSelectFork() internal { // Check if the PROVIDER_URL is set. require(vm.envExists("PROVIDER_URL"), "PROVIDER_URL not set"); @@ -90,6 +104,14 @@ abstract contract Fork_Shared_Test_ is Modifiers { oethARM = OEthARM(address(proxy)); } + // function _deployContracts() internal { + // proxy = Proxy(deployManager.getDeployment("OETH_ARM")); + // oethARM = OEthARM(deployManager.getDeployment("OETH_ARM")); + + // // Only fuzz from this address. Big speedup on fork. + // targetSender(address(this)); + // } + function _label() internal { vm.label(address(oeth), "OETH"); vm.label(address(weth), "WETH");