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 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a2dff6..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 --fork-url $PROVIDER_URL -vvv + run: forge test -vvv --summary --detailed diff --git a/.gitignore b/.gitignore index 84e39b7..3b14d8d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ bin/ !/broadcast /broadcast/*/31337/ /broadcast/**/dry-run/ +broadcast logs # Dotenv file @@ -21,6 +22,10 @@ yarn.lock cache_hardhat artifacts +# Forge +.gas-snapshot +build/ + # Defender Actions dist diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d9dddf0 --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +-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: + @forge test --gas-report + +# Generate gas snapshots for all your test functions +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 + +# Deploy contract +simulate-c-%: + @forge script script/$*.s.sol --fork-url $(PROVIDER_URL) -vvvvv + +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 diff --git a/README.md b/README.md index 00e1ac7..9f5b11b 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,48 @@ 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. + +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 -### Running gas report scripts +``` +make test-f-TestFunctionName +``` + +Report gas usage for tests: + +``` +make gas +``` ## Open Zeppelin Defender [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. @@ -54,15 +84,49 @@ 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. -## Deployment +#### For smart contract -### Dry-run on fork ``` -$ DEPLOYER_PRIVATE_KEY=$DEPLOYER_PRIVATE_KEY forge script script/deploy/DeployManager.sol:DeployManager --fork-url $ALCHEMY_PROVIDER_URL +make simulate-c-ScriptContractName +# example: make simulate-c-001_OETH_ARM ``` -### Deployment with Verification +#### 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 ``` -$ 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 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/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 diff --git a/script/001_OETH_ARM.s.sol b/script/001_OETH_ARM.s.sol new file mode 100644 index 0000000..4f1bbc7 --- /dev/null +++ b/script/001_OETH_ARM.s.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +import {console} from "forge-std/console.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"; + +/// @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 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"); + } + } + + ////////////////////////////////////////////////////// + /// --- 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); + + // 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/999_Tasks.s.sol b/script/999_Tasks.s.sol new file mode 100644 index 0000000..d885df8 --- /dev/null +++ b/script/999_Tasks.s.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Foundry +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 _999_TasksScript 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); + + // 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(); + } +} 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/src/contracts/Interfaces.sol b/src/contracts/Interfaces.sol index 1ddca63..bd79ddd 100644 --- a/src/contracts/Interfaces.sol +++ b/src/contracts/Interfaces.sol @@ -9,6 +9,8 @@ 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 +50,18 @@ 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); + + function withdrawalQueueMetadata() + external + returns (uint128 queued, uint128 claimable, uint128 claimed, uint128 nextWithdrawalIndex); + + function CLAIM_DELAY() external returns (uint256); } interface IGovernance { 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/Base.sol b/test/Base.sol new file mode 100644 index 0000000..e6f9620 --- /dev/null +++ b/test/Base.sol @@ -0,0 +1,54 @@ +// 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 "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 + ////////////////////////////////////////////////////// + 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/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/concrete/Ownable.t.sol b/test/fork/concrete/Ownable.t.sol new file mode 100644 index 0000000..324e9ac --- /dev/null +++ b/test/fork/concrete/Ownable.t.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +/// @notice The purpose of this contract is to test the `Ownable` contract. +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 asOwner { + // Assertions before + assertEq(oethARM.operator(), address(0)); + + oethARM.setOperator(operator); + + // Assertions after + assertEq(oethARM.operator(), operator); + } + + function test_SetOwner() public asOwner { + // Assertions before + assertEq(oethARM.owner(), Mainnet.TIMELOCK); + + 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..1809671 --- /dev/null +++ b/test/fork/concrete/Proxy.t.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Contracts +import {OEthARM} from "contracts/OethARM.sol"; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Utils +import {Mainnet} from "test/utils/Addresses.sol"; + +/// @notice The purpose of this contract is to test the `Proxy` contract. +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 asOwner { + address owner = Mainnet.TIMELOCK; + + // Deploy new implementation + OEthARM newImplementation = new OEthARM(); + 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 asOwner { + address owner = Mainnet.TIMELOCK; + + // Deploy new implementation + OEthARM newImplementation = new OEthARM(); + bytes memory data = abi.encodeWithSignature("setOperator(address)", address(0x123)); + + 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..36a798e --- /dev/null +++ b/test/fork/concrete/SwapExactTokensForTokens.t.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +/// @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 + ////////////////////////////////////////////////////// + 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_InsufficientOutputAmount() 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_InvalidSwap_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_InvalidSwap_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..8487ada --- /dev/null +++ b/test/fork/concrete/SwapTokensForExactTokens.t.sol @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +/// @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 + ////////////////////////////////////////////////////// + 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_InsufficientOutputAmount() 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_InsufficientOutputAmount() 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_InvalidSwap_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_InvalidSwap_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..bb4e394 --- /dev/null +++ b/test/fork/concrete/Transfer.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +/// @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 shouldRevertOnReceive; + + ////////////////////////////////////////////////////// + /// --- 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 asOwner { + shouldRevertOnReceive = true; + + vm.expectRevert("ARM: ETH transfer failed"); + oethARM.transferEth(address(this), 10 ether); + } + + ////////////////////////////////////////////////////// + /// --- PASSING TESTS + ////////////////////////////////////////////////////// + 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); + 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 asOwner { + // Assertions before + uint256 balanceBefore = address(this).balance; + assertEq(address(oethARM).balance, 100 ether); + + 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 (shouldRevertOnReceive) revert(); + } +} diff --git a/test/fork/concrete/Withdraw.t.sol b/test/fork/concrete/Withdraw.t.sol new file mode 100644 index 0000000..45e3382 --- /dev/null +++ b/test/fork/concrete/Withdraw.t.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.23; + +// Test imports +import {Fork_Shared_Test_} from "test/fork/shared/Shared.sol"; + +// Interfaces +import {IERC20} from "contracts/Interfaces.sol"; + +/// @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_ { + ////////////////////////////////////////////////////// + /// --- 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); + } + + ////////////////////////////////////////////////////// + /// --- 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 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, 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"); + } + + function test_ClaimWithdraw_() public asOwner mockCallDripperCollect { + // 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 + oethARM.claimWithdrawal(requestId); + + // Assertions after + assertEq(weth.balanceOf(address(oethARM)), 1 ether, "WETH balance should be 1 ether"); + } + + 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] = nextWithdrawalIndex; + requestIds[1] = nextWithdrawalIndex + 1; + // Then claim withdrawal + 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..35367f1 --- /dev/null +++ b/test/fork/shared/Shared.sol @@ -0,0 +1,126 @@ +// 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"; + +// 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"; + +/// @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). +/// - 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 unusual 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 Modifiers { + uint256 public forkId; + DeployManager public deployManager; + + ////////////////////////////////////////////////////// + /// --- SETUP + ////////////////////////////////////////////////////// + function setUp() public virtual override { + super.setUp(); + + // 1. Create deploy manager. + // _createDeployManager(); + + // 2. Create fork. + _createAndSelectFork(); + + // 3. Create users. + _generateAddresses(); + + // 4. Deploy contracts. + _deployContracts(); + + // 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"); + + // 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 _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"); + 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"); + } +} 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); + } + } +} 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 new file mode 100644 index 0000000..acc8825 --- /dev/null +++ b/test/fork/utils/Modifiers.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +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. + modifier asOwner() { + vm.startPrank(oethARM.owner()); + _; + vm.stopPrank(); + } + + /// @notice Impersonate the governor of the vault. + modifier asGovernor() { + vm.startPrank(vault.governor()); + _; + vm.stopPrank(); + } + + /// @notice Mock the call to the dripper's `collect` function, bypass it and return `true`. + modifier mockCallDripperCollect() { + MockCall.mockCallDripperCollect(vault.dripper()); + _; + } +} diff --git a/test/utils/Addresses.sol b/test/utils/Addresses.sol new file mode 100644 index 0000000..2cafefc --- /dev/null +++ b/test/utils/Addresses.sol @@ -0,0 +1,20 @@ +// 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; + address public constant OETHARM = 0x8Ad159a275AEE56fb2334DBb69036E9c7baCEe9b; +}