From efee4743619c46c657456e6bf1da4ae79cb7cccf Mon Sep 17 00:00:00 2001 From: guzus Date: Sat, 16 Nov 2024 17:59:49 +0700 Subject: [PATCH] move directory --- packages/foundry/.env.example | 18 -- packages/foundry/.github/workflows/test.yml | 34 +++ packages/foundry/.gitignore | 8 +- packages/foundry/.gitmodules | 9 + packages/foundry/.prettier.json | 6 - packages/foundry/LICENSE | 21 ++ packages/foundry/Makefile | 89 ------ packages/foundry/README.md | 102 +++++++ packages/foundry/contracts/EtfHook.sol | 103 ------- packages/foundry/contracts/YourContract.sol | 88 ------ packages/foundry/deployments/.gitignore | 2 - packages/foundry/foundry.toml | 51 +--- packages/foundry/lib/forge-std | 2 +- packages/foundry/lib/openzeppelin-contracts | 1 - packages/foundry/lib/solidity-bytes-utils | 1 - packages/foundry/lib/v4-periphery | 1 + packages/foundry/package.json | 33 --- packages/foundry/remappings.txt | 8 +- packages/foundry/script/00_Counter.s.sol | 45 +++ .../01_CreatePoolAndMintLiquidity.s.sol | 122 ++++++++ .../foundry/script/01a_CreatePoolOnly.s.sol | 51 ++++ packages/foundry/script/02_AddLiquidity.s.sol | 88 ++++++ packages/foundry/script/03_Swap.s.sol | 65 +++++ packages/foundry/script/Anvil.s.sol | 172 +++++++++++ packages/foundry/script/Deploy.s.sol | 16 -- packages/foundry/script/DeployHelpers.s.sol | 127 --------- .../foundry/script/DeployYourContract.s.sol | 17 -- packages/foundry/script/VerifyAll.s.sol | 139 --------- packages/foundry/script/base/Config.sol | 17 ++ packages/foundry/script/base/Constants.sol | 16 ++ packages/foundry/script/mocks/MockER20.s.sol | 20 ++ packages/foundry/scripts-js/ListAccount.js | 110 -------- packages/foundry/scripts-js/generateTsAbis.js | 156 ---------- packages/foundry/src/EtfHook.sol | 172 +++++++++++ packages/foundry/src/EtfToken.sol | 33 +++ packages/foundry/test/Counter.t.sol | 124 ++++++++ packages/foundry/test/YourContract.t.sol | 20 -- .../test/custom-accounting/ExampleHook.sol | 58 ++++ .../test/custom-accounting/ExampleHook.t.sol | 155 ++++++++++ packages/foundry/test/utils/EasyPosm.sol | 197 +++++++++++++ packages/foundry/test/utils/EasyPosm.t.sol | 266 ++++++++++++++++++ packages/foundry/test/utils/Fixtures.sol | 88 ++++++ packages/foundry/test/utils/HookMiner.sol | 52 ++++ .../test/utils/forks/DeployPermit2.sol | 30 ++ .../test/utils/forks/Permit2Bytecode.sol | 7 + 45 files changed, 1968 insertions(+), 972 deletions(-) delete mode 100644 packages/foundry/.env.example create mode 100644 packages/foundry/.github/workflows/test.yml create mode 100644 packages/foundry/.gitmodules delete mode 100644 packages/foundry/.prettier.json create mode 100644 packages/foundry/LICENSE delete mode 100644 packages/foundry/Makefile create mode 100644 packages/foundry/README.md delete mode 100644 packages/foundry/contracts/EtfHook.sol delete mode 100644 packages/foundry/contracts/YourContract.sol delete mode 100644 packages/foundry/deployments/.gitignore delete mode 160000 packages/foundry/lib/openzeppelin-contracts delete mode 160000 packages/foundry/lib/solidity-bytes-utils create mode 160000 packages/foundry/lib/v4-periphery delete mode 100644 packages/foundry/package.json create mode 100644 packages/foundry/script/00_Counter.s.sol create mode 100644 packages/foundry/script/01_CreatePoolAndMintLiquidity.s.sol create mode 100644 packages/foundry/script/01a_CreatePoolOnly.s.sol create mode 100644 packages/foundry/script/02_AddLiquidity.s.sol create mode 100644 packages/foundry/script/03_Swap.s.sol create mode 100644 packages/foundry/script/Anvil.s.sol delete mode 100644 packages/foundry/script/Deploy.s.sol delete mode 100644 packages/foundry/script/DeployHelpers.s.sol delete mode 100644 packages/foundry/script/DeployYourContract.s.sol delete mode 100644 packages/foundry/script/VerifyAll.s.sol create mode 100644 packages/foundry/script/base/Config.sol create mode 100644 packages/foundry/script/base/Constants.sol create mode 100644 packages/foundry/script/mocks/MockER20.s.sol delete mode 100644 packages/foundry/scripts-js/ListAccount.js delete mode 100644 packages/foundry/scripts-js/generateTsAbis.js create mode 100644 packages/foundry/src/EtfHook.sol create mode 100644 packages/foundry/src/EtfToken.sol create mode 100644 packages/foundry/test/Counter.t.sol delete mode 100644 packages/foundry/test/YourContract.t.sol create mode 100644 packages/foundry/test/custom-accounting/ExampleHook.sol create mode 100644 packages/foundry/test/custom-accounting/ExampleHook.t.sol create mode 100644 packages/foundry/test/utils/EasyPosm.sol create mode 100644 packages/foundry/test/utils/EasyPosm.t.sol create mode 100644 packages/foundry/test/utils/Fixtures.sol create mode 100644 packages/foundry/test/utils/HookMiner.sol create mode 100644 packages/foundry/test/utils/forks/DeployPermit2.sol create mode 100644 packages/foundry/test/utils/forks/Permit2Bytecode.sol diff --git a/packages/foundry/.env.example b/packages/foundry/.env.example deleted file mode 100644 index bf10f5c..0000000 --- a/packages/foundry/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# Template for foundry environment variables. - -# For local development, copy this file, rename it to .env, and fill in the values. - -# We provide default values so developers can start prototyping out of the box, -# but we recommend getting your own API Keys for Production Apps. - -# DEPLOYER_PRIVATE_KEY is used while deploying contract. -# On anvil chain the value of it can be empty since we use the prefunded account -# which comes with anvil chain to deploy contract. -# NOTE: You don't need to manually change the value of DEPLOYER_PRIVATE_KEY, it should -# be auto filled when run `yarn generate`. -# Although `.env` is ignored by git, it's still important that you don't paste your -# actual account private key and use the generated one. -# Etherscan API key is used to verify the contract on etherscan. -ETHERSCAN_API_KEY=DNXJA8RX2Q3VZ4URQIWP7Z68CJXQZSC6AW -# Default account for localhost / use "scaffold-eth-custom" if you wish to use a generated account or imported account -ETH_KEYSTORE_ACCOUNT=scaffold-eth-default diff --git a/packages/foundry/.github/workflows/test.yml b/packages/foundry/.github/workflows/test.yml new file mode 100644 index 0000000..09880b1 --- /dev/null +++ b/packages/foundry/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/packages/foundry/.gitignore b/packages/foundry/.gitignore index 88f95b6..63ab6d6 100644 --- a/packages/foundry/.gitignore +++ b/packages/foundry/.gitignore @@ -2,8 +2,10 @@ cache/ out/ -# Ignores development broadcast logs +# Ignores development (and goerli) broadcast logs +!/broadcast /broadcast/*/31337/ +/broadcast/*/5/ /broadcast/**/dry-run/ # Docs @@ -11,4 +13,6 @@ docs/ # Dotenv file .env -localhost.json + +# Node modules +node_modules/ \ No newline at end of file diff --git a/packages/foundry/.gitmodules b/packages/foundry/.gitmodules new file mode 100644 index 0000000..a825731 --- /dev/null +++ b/packages/foundry/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "lib/v4-core"] + path = lib/v4-core + url = https://github.com/uniswap/v4-core +[submodule "lib/v4-periphery"] + path = lib/v4-periphery + url = https://github.com/uniswap/v4-periphery diff --git a/packages/foundry/.prettier.json b/packages/foundry/.prettier.json deleted file mode 100644 index 7eef1ab..0000000 --- a/packages/foundry/.prettier.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "arrowParens": "avoid", - "printWidth": 120, - "tabWidth": 2, - "trailingComma": "all" -} diff --git a/packages/foundry/LICENSE b/packages/foundry/LICENSE new file mode 100644 index 0000000..381ddcd --- /dev/null +++ b/packages/foundry/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 saucepoint + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/foundry/Makefile b/packages/foundry/Makefile deleted file mode 100644 index 00144c4..0000000 --- a/packages/foundry/Makefile +++ /dev/null @@ -1,89 +0,0 @@ -.PHONY: build deploy generate-abis verify-keystore account chain compile deploy-verify flatten fork format lint test verify - -# setup wallet for anvil -setup-anvil-wallet: - shx rm ~/.foundry/keystores/scaffold-eth-default 2>/dev/null; \ - cast wallet import --private-key 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 --unsafe-password 'localhost' scaffold-eth-default - -# Start local chain -chain: setup-anvil-wallet - anvil - -# Start a fork -fork: setup-anvil-wallet - anvil --fork-url ${FORK_URL} --chain-id 31337 - -# Build the project -build: - forge build --build-info --build-info-path out/build-info/ - -# Deploy the project -deploy: - @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi; \ - else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi; \ - fi - -# Build and deploy target -build-and-deploy: build deploy generate-abis - -# Generate TypeScript ABIs -generate-abis: - node scripts-js/generateTsAbis.js - -verify-keystore: - if grep -q "scaffold-eth-default" .env; then \ - cast wallet address --password localhost; \ - else \ - cast wallet address; \ - fi - -# List account -account: - @node scripts-js/ListAccount.js $$(make verify-keystore) - -# Generate a new account -account-generate: - @cast wallet import $(ACCOUNT_NAME) --private-key $$(cast wallet new | grep 'Private key:' | awk '{print $$3}') - @echo "Please update .env file with ETH_KEYSTORE_ACCOUNT=$(ACCOUNT_NAME)" - -# Import an existing account -account-import: - @cast wallet import ${ACCOUNT_NAME} --interactive - -# Compile contracts -compile: - forge compile - -# Deploy and verify -deploy-verify: - @if [ "$(RPC_URL)" = "localhost" ]; then \ - forge script script/Deploy.s.sol --rpc-url localhost --password localhost --broadcast --legacy --ffi --verify; \ - else \ - forge script script/Deploy.s.sol --rpc-url $(RPC_URL) --broadcast --legacy --ffi --verify; \ - fi - node scripts-js/generateTsAbis.js - -# Flatten contracts -flatten: - forge flatten - -# Format code -format: - forge fmt && prettier --write ./scripts-js/**/*.js - -# Lint code -lint: - forge fmt --check && prettier --check ./script/**/*.js - -# Run tests -test: - forge test - -# Verify contracts -verify: - forge script script/VerifyAll.s.sol --ffi --rpc-url $(RPC_URL) - -build-and-verify: build verify - diff --git a/packages/foundry/README.md b/packages/foundry/README.md new file mode 100644 index 0000000..99b11c9 --- /dev/null +++ b/packages/foundry/README.md @@ -0,0 +1,102 @@ +# v4-template +### **A template for writing Uniswap v4 Hooks 🦄** + +[`Use this Template`](https://github.com/uniswapfoundation/v4-template/generate) + +1. The example hook [Counter.sol](src/Counter.sol) demonstrates the `beforeSwap()` and `afterSwap()` hooks +2. The test template [Counter.t.sol](test/Counter.t.sol) preconfigures the v4 pool manager, test tokens, and test liquidity. + +
+Updating to v4-template:latest + +This template is actively maintained -- you can update the v4 dependencies, scripts, and helpers: +```bash +git remote add template https://github.com/uniswapfoundation/v4-template +git fetch template +git merge template/main --allow-unrelated-histories +``` + +
+ +--- + +### Check Forge Installation +*Ensure that you have correctly installed Foundry (Forge) and that it's up to date. You can update Foundry by running:* + +``` +foundryup +``` + +## Set up + +*requires [foundry](https://book.getfoundry.sh)* + +``` +forge install +forge test +``` + +## Run script + +``` +forge script script/00_Counter.s.sol --tc EtfHookScript --rpc-url https://sepolia.base.org +``` + +### Local Development (Anvil) + +Other than writing unit tests (recommended!), you can only deploy & test hooks on [anvil](https://book.getfoundry.sh/anvil/) + +```bash +# start anvil, a local EVM chain +anvil + +# in a new terminal +forge script script/Anvil.s.sol \ + --rpc-url http://localhost:8545 \ + --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ + --broadcast +``` + +See [script/](script/) for hook deployment, pool creation, liquidity provision, and swapping. + +--- + +
+

Troubleshooting

+ + + +### *Permission Denied* + +When installing dependencies with `forge install`, Github may throw a `Permission Denied` error + +Typically caused by missing Github SSH keys, and can be resolved by following the steps [here](https://docs.github.com/en/github/authenticating-to-github/connecting-to-github-with-ssh) + +Or [adding the keys to your ssh-agent](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#adding-your-ssh-key-to-the-ssh-agent), if you have already uploaded SSH keys + +### Hook deployment failures + +Hook deployment failures are caused by incorrect flags or incorrect salt mining + +1. Verify the flags are in agreement: + * `getHookCalls()` returns the correct flags + * `flags` provided to `HookMiner.find(...)` +2. Verify salt mining is correct: + * In **forge test**: the *deployer* for: `new Hook{salt: salt}(...)` and `HookMiner.find(deployer, ...)` are the same. This will be `address(this)`. If using `vm.prank`, the deployer will be the pranking address + * In **forge script**: the deployer must be the CREATE2 Proxy: `0x4e59b44847b379578588920cA78FbF26c0B4956C` + * If anvil does not have the CREATE2 deployer, your foundry may be out of date. You can update it with `foundryup` + +
+ +--- + +Additional resources: + +[Uniswap v4 docs](https://docs.uniswap.org/contracts/v4/overview) + +[v4-periphery](https://github.com/uniswap/v4-periphery) contains advanced hook implementations that serve as a great reference + +[v4-core](https://github.com/uniswap/v4-core) + +[v4-by-example](https://v4-by-example.org) + diff --git a/packages/foundry/contracts/EtfHook.sol b/packages/foundry/contracts/EtfHook.sol deleted file mode 100644 index 499f473..0000000 --- a/packages/foundry/contracts/EtfHook.sol +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -contract ETFHook is BaseHook { - address[] public tokens; // the underlying tokens will be stored in this hook contract - uint256[] public weights; - uint256 public rebalanceThreshold; - - uint256[] public tokenBalances; - - constructor( - IPoolManager _poolManager, - address[] memory _tokens, - uint256[] memory _weights, - uint256[] memory _rebalanceThreshold - ) BaseHook(_poolManager) { - tokens = _tokens; - weights = _weights; - rebalanceThreshold = _rebalanceThreshold; - for (int = 0; i < len(_tokens); i++) { - tokenBalances[i] = 0; - } - } - - function getHooksCalls() public pure override returns (Hooks.Calls memory) { - return - Hooks.Calls({ - beforeInitialize: false, - afterInitialize: false, - beforeAddLiquidity: true, // rebalance ETF, mints ETF token - afterAddLiquidity: true, // rebalance ETF, burns ETF token - beforeModifyPosition: false, - afterModifyPosition: false, - beforeSwap: true, // rebalance ETF - afterSwap: false, - beforeDonate: false, - afterDonate: false - }); - } - - function beforeSwap( - address sender, - IPoolManager.PoolKey calldata, - IPoolManager.SwapParams calldata - ) external override returns (bytes4) { - if (checkIfRebalanceNeeded()) { - rebalance(); - } - return BaseHook.beforeSwap.selector; - } - - function beforeAddLiquidity( - address sender, - IPoolManager.PoolKey calldata, - IPoolManager.AddLiquidityParams calldata - ) external override returns (bytes4) { - if (checkIfRebalanceNeeded()) { - rebalance(); - } - mintETFToken(); - return BaseHook.beforeAddLiquidity.selector; - } - - function beforeRemoveLiquidity( - address sender, - IPoolManager.PoolKey calldata, - IPoolManager.AddLiquidityParams calldata - ) external override returns (bytes4) { - if (checkIfRebalanceNeeded()) { - rebalance(); - } - burnETFToken(); - return BaseHook.beforeRemoveLiquidity.selector; - } - - // returns each token prices from oracle - function getPrices() public returns (uint256[] prices) { - // TODO: use chainlink, pyth, chronicle - return; - } - - function checkIfRebalanceNeeded() private returns (bool) { - // check chainlink if we need to rebalance (check if rebalanceThreshold is reached) - // return true if rebalance needed - uint256[] memory prices = getPrices(); - } - - function rebalance() private { - // sell A & buy B through specified uniswap pool - } - - function mintETFToken() private { - // transfer tokens to ETF pool contract - // update token balances - // mint ETF token to msg.sender - } - - function burnETFToken() private { - // transfer tokens to msg.sender - // update token balances - // burn ETF token from msg.sender - } -} diff --git a/packages/foundry/contracts/YourContract.sol b/packages/foundry/contracts/YourContract.sol deleted file mode 100644 index fde85a7..0000000 --- a/packages/foundry/contracts/YourContract.sol +++ /dev/null @@ -1,88 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity >=0.8.0 <0.9.0; - -// Useful for debugging. Remove when deploying to a live network. -import "forge-std/console.sol"; - -// Use openzeppelin to inherit battle-tested implementations (ERC20, ERC721, etc) -// import "@openzeppelin/contracts/access/Ownable.sol"; - -/** - * A smart contract that allows changing a state variable of the contract and tracking the changes - * It also allows the owner to withdraw the Ether in the contract - * @author BuidlGuidl - */ -contract YourContract { - // State Variables - address public immutable owner; - string public greeting = "Building Unstoppable Apps!!!"; - bool public premium = false; - uint256 public totalCounter = 0; - mapping(address => uint256) public userGreetingCounter; - - // Events: a way to emit log statements from smart contract that can be listened to by external parties - event GreetingChange( - address indexed greetingSetter, - string newGreeting, - bool premium, - uint256 value - ); - - // Constructor: Called once on contract deployment - // Check packages/foundry/deploy/Deploy.s.sol - constructor( - address _owner - ) { - owner = _owner; - } - - // Modifier: used to define a set of rules that must be met before or after a function is executed - // Check the withdraw() function - modifier isOwner() { - // msg.sender: predefined variable that represents address of the account that called the current function - require(msg.sender == owner, "Not the Owner"); - _; - } - - /** - * Function that allows anyone to change the state variable "greeting" of the contract and increase the counters - * - * @param _newGreeting (string memory) - new greeting to save on the contract - */ - function setGreeting( - string memory _newGreeting - ) public payable { - // Print data to the anvil chain console. Remove when deploying to a live network. - - console.logString("Setting new greeting"); - console.logString(_newGreeting); - - greeting = _newGreeting; - totalCounter += 1; - userGreetingCounter[msg.sender] += 1; - - // msg.value: built-in global variable that represents the amount of ether sent with the transaction - if (msg.value > 0) { - premium = true; - } else { - premium = false; - } - - // emit: keyword used to trigger an event - emit GreetingChange(msg.sender, _newGreeting, msg.value > 0, msg.value); - } - - /** - * Function that allows the owner to withdraw all the Ether in the contract - * The function can only be called by the owner of the contract as defined by the isOwner modifier - */ - function withdraw() public isOwner { - (bool success,) = owner.call{ value: address(this).balance }(""); - require(success, "Failed to send Ether"); - } - - /** - * Function that allows the contract to receive ETH - */ - receive() external payable { } -} diff --git a/packages/foundry/deployments/.gitignore b/packages/foundry/deployments/.gitignore deleted file mode 100644 index e308ae9..0000000 --- a/packages/foundry/deployments/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Ignore 31337 deployments -31337.json diff --git a/packages/foundry/foundry.toml b/packages/foundry/foundry.toml index e21cbf8..62e56dc 100644 --- a/packages/foundry/foundry.toml +++ b/packages/foundry/foundry.toml @@ -1,43 +1,10 @@ [profile.default] -src = 'contracts' -out = 'out' -libs = ['lib'] -fs_permissions = [{ access = "read-write", path = "./"}] - -[rpc_endpoints] -default_network = "http://127.0.0.1:8545" -localhost = "http://127.0.0.1:8545" - -mainnet = "https://cloudflare-eth.com" -sepolia = "https://rpc2.sepolia.org" -arbitrum = "https://arb1.arbitrum.io/rpc" -arbitrumSepolia = "https://sepolia-rollup.arbitrum.io/rpc" -optimism = "https://mainnet.optimism.io" -optimismSepolia = "https://sepolia.optimism.io" -polygon = "https://polygon-rpc.com" -polygonMumbai = "https://rpc.ankr.com/polygon_mumbai" -polygonZkEvm = "https://zkevm-rpc.com" -polygonZkEvmTestnet = "https://rpc.public.zkevm-test.net" -gnosis = "https://rpc.gnosischain.com" -chiado = "https://rpc.chiadochain.net" -base = "https://mainnet.base.org" -baseSepolia = "https://sepolia.base.org" -scrollSepolia = "https://sepolia-rpc.scroll.io" -scroll = "https://rpc.scroll.io" -pgn = "https://rpc.publicgoods.network" -pgnTestnet = "https://sepolia.publicgoods.network" - -[etherscan] -polygonMumbai = { key = "${ETHERSCAN_API_KEY}" } -sepolia = { key = "${ETHERSCAN_API_KEY}" } - - -[fmt] -multiline_func_header = "params_first" -line_length = 80 -tab_width = 2 -quote_style = "double" -bracket_spacing = true -int_types = "long" - -# See more config options https://github.com/foundry-rs/foundry/tree/master/config +src = "src" +out = "out" +libs = ["lib"] +ffi = true +fs_permissions = [{ access = "read-write", path = ".forge-snapshots/"}] +solc_version = "0.8.26" +evm_version = "cancun" + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/packages/foundry/lib/forge-std b/packages/foundry/lib/forge-std index 1eea5ba..6e05729 160000 --- a/packages/foundry/lib/forge-std +++ b/packages/foundry/lib/forge-std @@ -1 +1 @@ -Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 +Subproject commit 6e05729b76f1ae0d437e74951aef1ca987788ab3 diff --git a/packages/foundry/lib/openzeppelin-contracts b/packages/foundry/lib/openzeppelin-contracts deleted file mode 160000 index 69c8def..0000000 --- a/packages/foundry/lib/openzeppelin-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 69c8def5f222ff96f2b5beff05dfba996368aa79 diff --git a/packages/foundry/lib/solidity-bytes-utils b/packages/foundry/lib/solidity-bytes-utils deleted file mode 160000 index e0115c4..0000000 --- a/packages/foundry/lib/solidity-bytes-utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e0115c4d231910df47ce3b60625ce562fe4af985 diff --git a/packages/foundry/lib/v4-periphery b/packages/foundry/lib/v4-periphery new file mode 160000 index 0000000..2768c3d --- /dev/null +++ b/packages/foundry/lib/v4-periphery @@ -0,0 +1 @@ +Subproject commit 2768c3d578a5ffeec0bc51abcd8e472cb68f8a93 diff --git a/packages/foundry/package.json b/packages/foundry/package.json deleted file mode 100644 index 302c263..0000000 --- a/packages/foundry/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "@se-2/foundry", - "version": "0.0.1", - "type": "module", - "scripts": { - "account": "make account", - "account:generate": "make account-generate ACCOUNT_NAME=${1:-scaffold-eth-custom}", - "account:import": "make account-import ACCOUNT_NAME=${1:-scaffold-eth-custom}", - "chain": "make chain", - "compile": "make compile", - "deploy": "make build-and-deploy RPC_URL=${1:-localhost}", - "deploy:verify": "make deploy-verify RPC_URL=${1:-localhost}", - "flatten": "make flatten", - "fork": "make fork FORK_URL=${1:-mainnet}", - "format": "make format", - "postinstall": "shx cp -n .env.example .env", - "lint": "make lint", - "test": "make test", - "verify": "make build-and-verify RPC_URL=${1:-localhost}", - "verify-keystore": "make verify-keystore" - }, - "dependencies": { - "dotenv": "~16.3.1", - "envfile": "~6.18.0", - "ethers": "~5.7.1", - "prettier": "~2.8.8", - "qrcode": "~1.5.3", - "toml": "~3.0.0" - }, - "devDependencies": { - "shx": "~0.3.4" - } -} diff --git a/packages/foundry/remappings.txt b/packages/foundry/remappings.txt index df3cb81..466e4ce 100644 --- a/packages/foundry/remappings.txt +++ b/packages/foundry/remappings.txt @@ -1 +1,7 @@ -@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts +@uniswap/v4-core/=lib/v4-core/ +forge-gas-snapshot/=lib/v4-core/lib/forge-gas-snapshot/src/ +forge-std/=lib/v4-core/lib/forge-std/src/ +permit2/=lib/v4-periphery/lib/permit2/ +solmate/=lib/v4-core/lib/solmate/ +v4-core/=lib/v4-core/ +@v4-periphery/=lib/v4-periphery/ \ No newline at end of file diff --git a/packages/foundry/script/00_Counter.s.sol b/packages/foundry/script/00_Counter.s.sol new file mode 100644 index 0000000..2d78998 --- /dev/null +++ b/packages/foundry/script/00_Counter.s.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; + +import {Constants} from "./base/Constants.sol"; +import {ETFHook} from "../src/EtfHook.sol"; +import {HookMiner} from "../test/utils/HookMiner.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +/// @notice Mines the address and deploys the EtfHook.sol Hook contract +contract EtfHookScript is Script, Constants { + MockERC20 public token0; + MockERC20 public token1; + + function setUp() public {} + + function run() public { + // hook contracts must have specific flags encoded in the address + uint160 flags = uint160( + Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG + | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG + ); + + // Mine a salt that will produce a hook address with the correct flags + bytes memory constructorArgs = abi.encode(POOLMANAGER); + (address hookAddress, bytes32 salt) = + HookMiner.find(CREATE2_DEPLOYER, flags, type(ETFHook).creationCode, constructorArgs); + + // Deploy the hook using CREATE2 + vm.broadcast(); + + token0 = new MockERC20("Token0", "T0", 18); + token1 = new MockERC20("Token1", "T1", 18); + token0.mint(msg.sender, 1_000_000); + token1.mint(msg.sender, 1_000_000); + address[2] memory TOKENS = [address(token0), address(token1)]; + uint256[2] memory WEIGHTS = [uint256(1), uint256(1)]; + uint256 REBALANCE_THRESHOLD = 5; + ETFHook etfHook = new ETFHook{salt: salt}(IPoolManager(POOLMANAGER), TOKENS, WEIGHTS, REBALANCE_THRESHOLD); + require(address(etfHook) == hookAddress, "EtfHookScript: hook address mismatch"); + } +} diff --git a/packages/foundry/script/01_CreatePoolAndMintLiquidity.s.sol b/packages/foundry/script/01_CreatePoolAndMintLiquidity.s.sol new file mode 100644 index 0000000..106a117 --- /dev/null +++ b/packages/foundry/script/01_CreatePoolAndMintLiquidity.s.sol @@ -0,0 +1,122 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.20; + +// import "forge-std/Script.sol"; +// import {PositionManager} from "v4-periphery/src/PositionManager.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +// import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +// import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; +// import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +// import {IERC20} from "forge-std/interfaces/IERC20.sol"; + +// import {Constants} from "./base/Constants.sol"; +// import {Config} from "./base/Config.sol"; + +// contract CreatePoolAndAddLiquidityScript is Script, Constants, Config { +// using CurrencyLibrary for Currency; + +// ///////////////////////////////////// +// // --- Parameters to Configure --- // +// ///////////////////////////////////// + +// // --- pool configuration --- // +// // fees paid by swappers that accrue to liquidity providers +// uint24 lpFee = 3000; // 0.30% +// int24 tickSpacing = 60; + +// // starting price of the pool, in sqrtPriceX96 +// uint160 startingPrice = 79228162514264337593543950336; // floor(sqrt(1) * 2^96) + +// // --- liquidity position configuration --- // +// uint256 public token0Amount = 1e18; +// uint256 public token1Amount = 1e18; + +// // range of the position +// int24 tickLower = -600; // must be a multiple of tickSpacing +// int24 tickUpper = 600; +// ///////////////////////////////////// + +// function run() external { +// // tokens should be sorted +// PoolKey memory pool = PoolKey({ +// currency0: currency0, +// currency1: currency1, +// fee: lpFee, +// tickSpacing: tickSpacing, +// hooks: hookContract +// }); +// bytes memory hookData = new bytes(0); + +// // --------------------------------- // + +// // Converts token amounts to liquidity units +// uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( +// startingPrice, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// token0Amount, +// token1Amount +// ); + +// // slippage limits +// uint256 amount0Max = token0Amount + 1 wei; +// uint256 amount1Max = token1Amount + 1 wei; + +// (bytes memory actions, bytes[] memory mintParams) = +// _mintLiquidityParams(pool, tickLower, tickUpper, liquidity, amount0Max, amount1Max, address(this), hookData); + +// // multicall parameters +// bytes[] memory params = new bytes[](2); + +// // initialize pool +// params[0] = abi.encodeWithSelector(posm.initializePool.selector, pool, startingPrice, hookData); + +// // mint liquidity +// params[1] = abi.encodeWithSelector( +// posm.modifyLiquidities.selector, abi.encode(actions, mintParams), block.timestamp + 60 +// ); + +// // if the pool is an ETH pair, native tokens are to be transferred +// uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; + +// vm.startBroadcast(); +// tokenApprovals(); +// vm.stopBroadcast(); + +// // multicall to atomically create pool & add liquidity +// vm.broadcast(); +// posm.multicall{value: valueToPass}(params); +// } + +// /// @dev helper function for encoding mint liquidity operation +// /// @dev does NOT encode SWEEP, developers should take care when minting liquidity on an ETH pair +// function _mintLiquidityParams( +// PoolKey memory poolKey, +// int24 _tickLower, +// int24 _tickUpper, +// uint256 liquidity, +// uint256 amount0Max, +// uint256 amount1Max, +// address recipient, +// bytes memory hookData +// ) internal pure returns (bytes memory, bytes[] memory) { +// bytes memory actions = abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR)); + +// bytes[] memory params = new bytes[](2); +// params[0] = abi.encode(poolKey, _tickLower, _tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData); +// params[1] = abi.encode(poolKey.currency0, poolKey.currency1); +// return (actions, params); +// } + +// function tokenApprovals() public { +// if (!currency0.isAddressZero()) { +// token0.approve(address(PERMIT2), type(uint256).max); +// PERMIT2.approve(address(token0), address(posm), type(uint160).max, type(uint48).max); +// } +// if (!currency1.isAddressZero()) { +// token1.approve(address(PERMIT2), type(uint256).max); +// PERMIT2.approve(address(token1), address(posm), type(uint160).max, type(uint48).max); +// } +// } +// } diff --git a/packages/foundry/script/01a_CreatePoolOnly.s.sol b/packages/foundry/script/01a_CreatePoolOnly.s.sol new file mode 100644 index 0000000..fee7cde --- /dev/null +++ b/packages/foundry/script/01a_CreatePoolOnly.s.sol @@ -0,0 +1,51 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.20; + +// import "forge-std/Script.sol"; +// import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; + +// import {Constants} from "./base/Constants.sol"; +// import {Config} from "./base/Config.sol"; + +// contract CreatePoolOnly is Script, Constants, Config { +// using CurrencyLibrary for Currency; + +// // NOTE: Be sure to set the addresses in Constants.sol and Config.sol + +// ///////////////////////////////////// +// // --- Parameters to Configure --- // +// ///////////////////////////////////// + +// // --- pool configuration --- // +// // fees paid by swappers that accrue to liquidity providers +// uint24 lpFee = 5000; // 0.30% +// int24 tickSpacing = 60; + +// // starting price of the pool, in sqrtPriceX96 +// uint160 startingPrice = 79228162514264337593543950336; // floor(sqrt(1) * 2^96) + +// // --- liquidity position configuration --- // +// uint256 public token0Amount = 1e18; +// uint256 public token1Amount = 1e18; + +// // range of the position +// int24 tickLower = -600; // must be a multiple of tickSpacing +// int24 tickUpper = 600; +// ///////////////////////////////////// + +// function run() external { +// PoolKey memory pool = PoolKey({ +// currency0: currency0, +// currency1: currency1, +// fee: lpFee, +// tickSpacing: tickSpacing, +// hooks: hookContract +// }); +// bytes memory hookData = new bytes(0); + +// vm.broadcast(); +// IPoolManager(POOLMANAGER).initialize(pool, startingPrice); +// } +// } diff --git a/packages/foundry/script/02_AddLiquidity.s.sol b/packages/foundry/script/02_AddLiquidity.s.sol new file mode 100644 index 0000000..4b9a83c --- /dev/null +++ b/packages/foundry/script/02_AddLiquidity.s.sol @@ -0,0 +1,88 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.20; + +// import "forge-std/Script.sol"; +// import "forge-std/console.sol"; +// import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +// import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +// import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; +// import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +// import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; + +// import {EasyPosm} from "../test/utils/EasyPosm.sol"; +// import {Constants} from "./base/Constants.sol"; +// import {Config} from "./base/Config.sol"; + +// contract AddLiquidityScript is Script, Constants, Config { +// using CurrencyLibrary for Currency; +// using EasyPosm for IPositionManager; +// using StateLibrary for IPoolManager; + +// ///////////////////////////////////// +// // --- Parameters to Configure --- // +// ///////////////////////////////////// + +// // --- pool configuration --- // +// // fees paid by swappers that accrue to liquidity providers +// uint24 lpFee = 3000; // 0.30% +// int24 tickSpacing = 60; + +// // --- liquidity position configuration --- // +// uint256 public token0Amount = 1e18; +// uint256 public token1Amount = 1e18; + +// // range of the position +// int24 tickLower = -600; // must be a multiple of tickSpacing +// int24 tickUpper = 600; +// ///////////////////////////////////// + +// function run() external { +// PoolKey memory pool = PoolKey({ +// currency0: currency0, +// currency1: currency1, +// fee: lpFee, +// tickSpacing: tickSpacing, +// hooks: hookContract +// }); + +// (uint160 sqrtPriceX96,,,) = POOLMANAGER.getSlot0(pool.toId()); + +// // Converts token amounts to liquidity units +// uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( +// sqrtPriceX96, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// token0Amount, +// token1Amount +// ); + +// // slippage limits +// uint256 amount0Max = token0Amount + 1 wei; +// uint256 amount1Max = token1Amount + 1 wei; + +// bytes memory hookData = new bytes(0); + +// vm.startBroadcast(); +// tokenApprovals(); +// vm.stopBroadcast(); + +// vm.startBroadcast(); +// IPositionManager(address(posm)).mint( +// pool, tickLower, tickUpper, liquidity, amount0Max, amount1Max, msg.sender, block.timestamp + 60, hookData +// ); +// vm.stopBroadcast(); +// } + +// function tokenApprovals() public { +// if (!currency0.isAddressZero()) { +// token0.approve(address(PERMIT2), type(uint256).max); +// PERMIT2.approve(address(token0), address(posm), type(uint160).max, type(uint48).max); +// } +// if (!currency1.isAddressZero()) { +// token1.approve(address(PERMIT2), type(uint256).max); +// PERMIT2.approve(address(token1), address(posm), type(uint160).max, type(uint48).max); +// } +// } +// } diff --git a/packages/foundry/script/03_Swap.s.sol b/packages/foundry/script/03_Swap.s.sol new file mode 100644 index 0000000..1c24b07 --- /dev/null +++ b/packages/foundry/script/03_Swap.s.sol @@ -0,0 +1,65 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.20; + +// import "forge-std/Script.sol"; +// import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; +// import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; + +// import {Constants} from "./base/Constants.sol"; +// import {Config} from "./base/Config.sol"; + +// contract SwapScript is Script, Constants, Config { +// // slippage tolerance to allow for unlimited price impact +// uint160 public constant MIN_PRICE_LIMIT = TickMath.MIN_SQRT_PRICE + 1; +// uint160 public constant MAX_PRICE_LIMIT = TickMath.MAX_SQRT_PRICE - 1; + +// ///////////////////////////////////// +// // --- Parameters to Configure --- // +// ///////////////////////////////////// + +// // PoolSwapTest Contract address, default to the anvil address +// PoolSwapTest swapRouter = PoolSwapTest(0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9); + +// // --- pool configuration --- // +// // fees paid by swappers that accrue to liquidity providers +// uint24 lpFee = 3000; // 0.30% +// int24 tickSpacing = 60; + +// function run() external { +// PoolKey memory pool = PoolKey({ +// currency0: currency0, +// currency1: currency1, +// fee: lpFee, +// tickSpacing: tickSpacing, +// hooks: hookContract +// }); + +// // approve tokens to the swap router +// vm.broadcast(); +// token0.approve(address(swapRouter), type(uint256).max); +// vm.broadcast(); +// token1.approve(address(swapRouter), type(uint256).max); + +// // ------------------------------ // +// // Swap 100e18 token0 into token1 // +// // ------------------------------ // +// bool zeroForOne = true; +// IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ +// zeroForOne: zeroForOne, +// amountSpecified: 100e18, +// sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT // unlimited impact +// }); + +// // in v4, users have the option to receieve native ERC20s or wrapped ERC1155 tokens +// // here, we'll take the ERC20s +// PoolSwapTest.TestSettings memory testSettings = +// PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + +// bytes memory hookData = new bytes(0); +// vm.broadcast(); +// swapRouter.swap(pool, params, testSettings, hookData); +// } +// } diff --git a/packages/foundry/script/Anvil.s.sol b/packages/foundry/script/Anvil.s.sol new file mode 100644 index 0000000..f6ce16c --- /dev/null +++ b/packages/foundry/script/Anvil.s.sol @@ -0,0 +1,172 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.19; + +// import "forge-std/Script.sol"; +// import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +// import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +// import {PoolManager} from "v4-core/src/PoolManager.sol"; +// import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +// import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol"; +// import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; +// import {PoolDonateTest} from "v4-core/src/test/PoolDonateTest.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +// import {Constants} from "v4-core/src/../test/utils/Constants.sol"; +// import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +// import {Counter} from "../src/Counter.sol"; +// import {HookMiner} from "../test/utils/HookMiner.sol"; +// import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +// import {PositionManager} from "v4-periphery/src/PositionManager.sol"; +// import {EasyPosm} from "../test/utils/EasyPosm.sol"; +// import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +// import {DeployPermit2} from "../test/utils/forks/DeployPermit2.sol"; +// import {IERC20} from "forge-std/interfaces/IERC20.sol"; +// import {IPositionDescriptor} from "v4-periphery/src/interfaces/IPositionDescriptor.sol"; +// import {IWETH9} from "v4-periphery/src/interfaces/external/IWETH9.sol"; + +// /// @notice Forge script for deploying v4 & hooks to **anvil** +// /// @dev This script only works on an anvil RPC because v4 exceeds bytecode limits +// contract CounterScript is Script, DeployPermit2 { +// using EasyPosm for IPositionManager; + +// address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + +// function setUp() public {} + +// function run() public { +// vm.broadcast(); +// IPoolManager manager = deployPoolManager(); + +// // hook contracts must have specific flags encoded in the address +// uint160 permissions = uint160( +// Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG +// | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG +// ); + +// // Mine a salt that will produce a hook address with the correct permissions +// (address hookAddress, bytes32 salt) = +// HookMiner.find(CREATE2_DEPLOYER, permissions, type(Counter).creationCode, abi.encode(address(manager))); + +// // ----------------------------- // +// // Deploy the hook using CREATE2 // +// // ----------------------------- // +// vm.broadcast(); +// Counter counter = new Counter{salt: salt}(manager); +// require(address(counter) == hookAddress, "CounterScript: hook address mismatch"); + +// // Additional helpers for interacting with the pool +// vm.startBroadcast(); +// IPositionManager posm = deployPosm(manager); +// (PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter,) = deployRouters(manager); +// vm.stopBroadcast(); + +// // test the lifecycle (create pool, add liquidity, swap) +// vm.startBroadcast(); +// testLifecycle(manager, address(counter), posm, lpRouter, swapRouter); +// vm.stopBroadcast(); +// } + +// // ----------------------------------------------------------- +// // Helpers +// // ----------------------------------------------------------- +// function deployPoolManager() internal returns (IPoolManager) { +// return IPoolManager(address(new PoolManager(address(0)))); +// } + +// function deployRouters(IPoolManager manager) +// internal +// returns (PoolModifyLiquidityTest lpRouter, PoolSwapTest swapRouter, PoolDonateTest donateRouter) +// { +// lpRouter = new PoolModifyLiquidityTest(manager); +// swapRouter = new PoolSwapTest(manager); +// donateRouter = new PoolDonateTest(manager); +// } + +// function deployPosm(IPoolManager poolManager) public returns (IPositionManager) { +// anvilPermit2(); +// return IPositionManager(new PositionManager(poolManager, permit2, 300_000, IPositionDescriptor(address(0)), IWETH9(address(0)))); +// } + +// function approvePosmCurrency(IPositionManager posm, Currency currency) internal { +// // Because POSM uses permit2, we must execute 2 permits/approvals. +// // 1. First, the caller must approve permit2 on the token. +// IERC20(Currency.unwrap(currency)).approve(address(permit2), type(uint256).max); +// // 2. Then, the caller must approve POSM as a spender of permit2 +// permit2.approve(Currency.unwrap(currency), address(posm), type(uint160).max, type(uint48).max); +// } + +// function deployTokens() internal returns (MockERC20 token0, MockERC20 token1) { +// MockERC20 tokenA = new MockERC20("MockA", "A", 18); +// MockERC20 tokenB = new MockERC20("MockB", "B", 18); +// if (uint160(address(tokenA)) < uint160(address(tokenB))) { +// token0 = tokenA; +// token1 = tokenB; +// } else { +// token0 = tokenB; +// token1 = tokenA; +// } +// } + +// function testLifecycle( +// IPoolManager manager, +// address hook, +// IPositionManager posm, +// PoolModifyLiquidityTest lpRouter, +// PoolSwapTest swapRouter +// ) internal { +// (MockERC20 token0, MockERC20 token1) = deployTokens(); +// token0.mint(msg.sender, 100_000 ether); +// token1.mint(msg.sender, 100_000 ether); + +// bytes memory ZERO_BYTES = new bytes(0); + +// // initialize the pool +// int24 tickSpacing = 60; +// PoolKey memory poolKey = +// PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, tickSpacing, IHooks(hook)); +// manager.initialize(poolKey, Constants.SQRT_PRICE_1_1); + +// // approve the tokens to the routers +// token0.approve(address(lpRouter), type(uint256).max); +// token1.approve(address(lpRouter), type(uint256).max); +// token0.approve(address(swapRouter), type(uint256).max); +// token1.approve(address(swapRouter), type(uint256).max); + +// approvePosmCurrency(posm, Currency.wrap(address(token0))); +// approvePosmCurrency(posm, Currency.wrap(address(token1))); + +// // add full range liquidity to the pool +// lpRouter.modifyLiquidity( +// poolKey, +// IPoolManager.ModifyLiquidityParams( +// TickMath.minUsableTick(tickSpacing), TickMath.maxUsableTick(tickSpacing), 100 ether, 0 +// ), +// ZERO_BYTES +// ); + +// posm.mint( +// poolKey, +// TickMath.minUsableTick(tickSpacing), +// TickMath.maxUsableTick(tickSpacing), +// 100e18, +// 10_000e18, +// 10_000e18, +// msg.sender, +// block.timestamp + 300, +// ZERO_BYTES +// ); + +// // swap some tokens +// bool zeroForOne = true; +// int256 amountSpecified = 1 ether; +// IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ +// zeroForOne: zeroForOne, +// amountSpecified: amountSpecified, +// sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1 // unlimited impact +// }); +// PoolSwapTest.TestSettings memory testSettings = +// PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); +// swapRouter.swap(poolKey, params, testSettings, ZERO_BYTES); +// } +// } diff --git a/packages/foundry/script/Deploy.s.sol b/packages/foundry/script/Deploy.s.sol deleted file mode 100644 index 8ca40a3..0000000 --- a/packages/foundry/script/Deploy.s.sol +++ /dev/null @@ -1,16 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "./DeployHelpers.s.sol"; -import { DeployYourContract } from "./DeployYourContract.s.sol"; - -contract DeployScript is ScaffoldETHDeploy { - function run() external { - DeployYourContract deployYourContract = new DeployYourContract(); - deployYourContract.run(); - - // deploy more contracts here - // DeployMyContract deployMyContract = new DeployMyContract(); - // deployMyContract.run(); - } -} diff --git a/packages/foundry/script/DeployHelpers.s.sol b/packages/foundry/script/DeployHelpers.s.sol deleted file mode 100644 index d8bacf6..0000000 --- a/packages/foundry/script/DeployHelpers.s.sol +++ /dev/null @@ -1,127 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import { Script, console } from "forge-std/Script.sol"; -import { Vm } from "forge-std/Vm.sol"; - -contract ScaffoldETHDeploy is Script { - error InvalidChain(); - error DeployerHasNoBalance(); - error InvalidPrivateKey(string); - - event AnvilSetBalance(address account, uint256 amount); - event FailedAnvilRequest(); - - struct Deployment { - string name; - address addr; - } - - string root; - string path; - Deployment[] public deployments; - uint256 constant ANVIL_BASE_BALANCE = 10000 ether; - - /// @notice The deployer address for every run - address deployer; - - /// @notice Use this modifier on your run() function on your deploy scripts - modifier ScaffoldEthDeployerRunner() { - deployer = _startBroadcast(); - if (deployer == address(0)) { - revert InvalidPrivateKey("Invalid private key"); - } - _; - _stopBroadcast(); - exportDeployments(); - } - - function _startBroadcast() internal returns (address) { - vm.startBroadcast(); - (, address _deployer,) = vm.readCallers(); - - if (block.chainid == 31337 && _deployer.balance == 0) { - try this.anvil_setBalance(_deployer, ANVIL_BASE_BALANCE) { - emit AnvilSetBalance(_deployer, ANVIL_BASE_BALANCE); - } catch { - emit FailedAnvilRequest(); - } - } - return _deployer; - } - - function _stopBroadcast() internal { - vm.stopBroadcast(); - } - - function exportDeployments() internal { - // fetch already existing contracts - root = vm.projectRoot(); - path = string.concat(root, "/deployments/"); - string memory chainIdStr = vm.toString(block.chainid); - path = string.concat(path, string.concat(chainIdStr, ".json")); - - string memory jsonWrite; - - uint256 len = deployments.length; - - for (uint256 i = 0; i < len; i++) { - vm.serializeString( - jsonWrite, vm.toString(deployments[i].addr), deployments[i].name - ); - } - - string memory chainName; - - try this.getChain() returns (Chain memory chain) { - chainName = chain.name; - } catch { - chainName = findChainName(); - } - jsonWrite = vm.serializeString(jsonWrite, "networkName", chainName); - vm.writeJson(jsonWrite, path); - } - - function getChain() public returns (Chain memory) { - return getChain(block.chainid); - } - - function anvil_setBalance(address addr, uint256 amount) public { - string memory addressString = vm.toString(addr); - string memory amountString = vm.toString(amount); - string memory requestPayload = string.concat( - '{"method":"anvil_setBalance","params":["', - addressString, - '","', - amountString, - '"],"id":1,"jsonrpc":"2.0"}' - ); - - string[] memory inputs = new string[](8); - inputs[0] = "curl"; - inputs[1] = "-X"; - inputs[2] = "POST"; - inputs[3] = "http://localhost:8545"; - inputs[4] = "-H"; - inputs[5] = "Content-Type: application/json"; - inputs[6] = "--data"; - inputs[7] = requestPayload; - - vm.ffi(inputs); - } - - function findChainName() public returns (string memory) { - uint256 thisChainId = block.chainid; - string[2][] memory allRpcUrls = vm.rpcUrls(); - for (uint256 i = 0; i < allRpcUrls.length; i++) { - try vm.createSelectFork(allRpcUrls[i][1]) { - if (block.chainid == thisChainId) { - return allRpcUrls[i][0]; - } - } catch { - continue; - } - } - revert InvalidChain(); - } -} diff --git a/packages/foundry/script/DeployYourContract.s.sol b/packages/foundry/script/DeployYourContract.s.sol deleted file mode 100644 index 547ed87..0000000 --- a/packages/foundry/script/DeployYourContract.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "../contracts/YourContract.sol"; -import "./DeployHelpers.s.sol"; - -contract DeployYourContract is ScaffoldETHDeploy { - // use `deployer` from `ScaffoldETHDeploy` - function run() external ScaffoldEthDeployerRunner { - YourContract yourContract = new YourContract(deployer); - console.logString( - string.concat( - "YourContract deployed at: ", vm.toString(address(yourContract)) - ) - ); - } -} diff --git a/packages/foundry/script/VerifyAll.s.sol b/packages/foundry/script/VerifyAll.s.sol deleted file mode 100644 index ec5b1e6..0000000 --- a/packages/foundry/script/VerifyAll.s.sol +++ /dev/null @@ -1,139 +0,0 @@ -//SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "forge-std/Script.sol"; -import "forge-std/Vm.sol"; -import "solidity-bytes-utils/BytesLib.sol"; - -/** - * @dev Temp Vm implementation - * @notice calls the tryffi function on the Vm contract - * @notice will be deleted once the forge/std is updated - */ -struct FfiResult { - int32 exit_code; - bytes stdout; - bytes stderr; -} - -interface tempVm { - function tryFfi( - string[] calldata - ) external returns (FfiResult memory); -} - -contract VerifyAll is Script { - uint96 currTransactionIdx; - - function run() external { - string memory root = vm.projectRoot(); - string memory path = string.concat( - root, - "/broadcast/Deploy.s.sol/", - vm.toString(block.chainid), - "/run-latest.json" - ); - string memory content = vm.readFile(path); - - while (this.nextTransaction(content)) { - _verifyIfContractDeployment(content); - currTransactionIdx++; - } - } - - function _verifyIfContractDeployment( - string memory content - ) internal { - string memory txType = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transactionType")), - (string) - ); - if (keccak256(bytes(txType)) == keccak256(bytes("CREATE"))) { - _verifyContract(content); - } - } - - function _verifyContract( - string memory content - ) internal { - string memory contractName = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractName")), - (string) - ); - address contractAddr = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "contractAddress")), - (address) - ); - bytes memory deployedBytecode = abi.decode( - vm.parseJson(content, searchStr(currTransactionIdx, "transaction.input")), - (bytes) - ); - bytes memory compiledBytecode = abi.decode( - vm.parseJson(_getCompiledBytecode(contractName), ".bytecode.object"), - (bytes) - ); - bytes memory constructorArgs = BytesLib.slice( - deployedBytecode, - compiledBytecode.length, - deployedBytecode.length - compiledBytecode.length - ); - - string[] memory inputs = new string[](9); - inputs[0] = "forge"; - inputs[1] = "verify-contract"; - inputs[2] = vm.toString(contractAddr); - inputs[3] = contractName; - inputs[4] = "--chain"; - inputs[5] = vm.toString(block.chainid); - inputs[6] = "--constructor-args"; - inputs[7] = vm.toString(constructorArgs); - inputs[8] = "--watch"; - - FfiResult memory f = tempVm(address(vm)).tryFfi(inputs); - - if (f.stderr.length != 0) { - console.logString( - string.concat( - "Submitting verification for contract: ", vm.toString(contractAddr) - ) - ); - console.logString(string(f.stderr)); - } else { - console.logString(string(f.stdout)); - } - return; - } - - function nextTransaction( - string memory content - ) external view returns (bool) { - try this.getTransactionFromRaw(content, currTransactionIdx) { - return true; - } catch { - return false; - } - } - - function _getCompiledBytecode( - string memory contractName - ) internal view returns (string memory compiledBytecode) { - string memory root = vm.projectRoot(); - string memory path = - string.concat(root, "/out/", contractName, ".sol/", contractName, ".json"); - compiledBytecode = vm.readFile(path); - } - - function getTransactionFromRaw( - string memory content, - uint96 idx - ) external pure { - abi.decode(vm.parseJson(content, searchStr(idx, "hash")), (bytes32)); - } - - function searchStr( - uint96 idx, - string memory searchKey - ) internal pure returns (string memory) { - return string.concat(".transactions[", vm.toString(idx), "].", searchKey); - } -} diff --git a/packages/foundry/script/base/Config.sol b/packages/foundry/script/base/Config.sol new file mode 100644 index 0000000..05c6583 --- /dev/null +++ b/packages/foundry/script/base/Config.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; + +/// @notice Shared configuration between scripts +contract Config { + /// @dev populated with default anvil addresses + IERC20 constant token0 = IERC20(address(0x0165878A594ca255338adfa4d48449f69242Eb8F)); + IERC20 constant token1 = IERC20(address(0xa513E6E4b8f2a923D98304ec87F64353C4D5C853)); + IHooks constant hookContract = IHooks(address(0x0)); + + Currency constant currency0 = Currency.wrap(address(token0)); + Currency constant currency1 = Currency.wrap(address(token1)); +} diff --git a/packages/foundry/script/base/Constants.sol b/packages/foundry/script/base/Constants.sol new file mode 100644 index 0000000..808722b --- /dev/null +++ b/packages/foundry/script/base/Constants.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PositionManager} from "v4-periphery/src/PositionManager.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +/// @notice Shared constants used in scripts +contract Constants { + address constant CREATE2_DEPLOYER = address(0x4e59b44847b379578588920cA78FbF26c0B4956C); + + /// @dev populated with default anvil addresses + IPoolManager constant POOLMANAGER = IPoolManager(address(0x5FbDB2315678afecb367f032d93F642f64180aa3)); + PositionManager constant posm = PositionManager(payable(address(0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0))); + IAllowanceTransfer constant PERMIT2 = IAllowanceTransfer(address(0x000000000022D473030F116dDEE9F6B43aC78BA3)); +} diff --git a/packages/foundry/script/mocks/MockER20.s.sol b/packages/foundry/script/mocks/MockER20.s.sol new file mode 100644 index 0000000..d19924c --- /dev/null +++ b/packages/foundry/script/mocks/MockER20.s.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Script.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; + +contract MockToken is MockERC20 { + constructor(string memory _name, string memory _symbol) MockERC20(_name, _symbol, 18) {} +} + +contract MockTokenScript is Script { + function setUp() public {} + + function run() public { + vm.startBroadcast(); + new MockToken("MockTokenA", "MOCKA"); + new MockToken("MockTokenB", "MOCKB"); + vm.stopBroadcast(); + } +} diff --git a/packages/foundry/scripts-js/ListAccount.js b/packages/foundry/scripts-js/ListAccount.js deleted file mode 100644 index a2c31fd..0000000 --- a/packages/foundry/scripts-js/ListAccount.js +++ /dev/null @@ -1,110 +0,0 @@ -import { config } from "dotenv"; -config(); -import { join, dirname } from "path"; -import { ethers, Wallet } from "ethers"; -import { toString } from "qrcode"; -import { readFileSync } from "fs"; -import { parse } from "toml"; -import { fileURLToPath } from "url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const ALCHEMY_API_KEY = - process.env.ALCHEMY_API_KEY || "oKxs-03sij-U_N0iOlrSsZFr29-IqbuF"; - -async function getBalanceForEachNetwork(address) { - try { - // Read the foundry.toml file - const foundryTomlPath = join(__dirname, "..", "foundry.toml"); - const tomlString = readFileSync(foundryTomlPath, "utf-8"); - - // Parse the tomlString to get the JS object representation - const parsedToml = parse(tomlString); - - // Extract rpc_endpoints from parsedToml - const rpcEndpoints = parsedToml.rpc_endpoints; - - // Replace placeholders in the rpc_endpoints section - function replaceENVAlchemyKey(input) { - return input.replace("${ALCHEMY_API_KEY}", ALCHEMY_API_KEY); - } - - for (const networkName in rpcEndpoints) { - if (networkName === "localhost") continue; - - const networkUrl = replaceENVAlchemyKey(rpcEndpoints[networkName]); - - try { - const provider = new ethers.providers.JsonRpcProvider(networkUrl); - const balance = await provider.getBalance(address); - console.log("--", networkName, "-- 📡"); - console.log(" balance:", +ethers.utils.formatEther(balance)); - console.log( - " nonce:", - +(await provider.getTransactionCount(address)) - ); - } catch (e) { - console.log("Can't connect to network", networkName); - console.log(); - } - } - } catch (error) { - console.error("Error reading foundry.toml:", error); - } -} - -function verifyAddressFormat(address) { - try { - ethers.utils.getAddress(address); - return true; - } catch (e) { - return false; - } -} - -function findAddressFromArgs(args) { - return args.find((arg) => arg.startsWith("0x") && arg.length === 42); -} - -const DEFAULT_KEYSTORE_ACCOUNT = "scaffold-eth-default"; -async function main() { - const address = findAddressFromArgs(process.argv); - - const isValidAddress = verifyAddressFormat(address); - const isDefaultAccount = - process.env.ETH_KEYSTORE_ACCOUNT === DEFAULT_KEYSTORE_ACCOUNT; - - if (!isValidAddress) { - console.log( - `\n🚫️ Unable to access keystore account ${process.env.ETH_KEYSTORE_ACCOUNT}` - ); - - if (isDefaultAccount) { - console.log( - "\n🏠 It seems you are trying to access the localhost account. Did you forget to update ETH_KEYSTORE_ACCOUNT=scaffold-eth-custom in the .env file?\n" - ); - } - - console.log( - "\n💡 If you haven't generated a deployer keystore account yet, please run `yarn account:generate`. Then update the `.env` file with `ETH_KEYSTORE_ACCOUNT=scaffold-eth-custom`" - ); - return; - } - - if (isValidAddress && isDefaultAccount) { - console.log("\n⚠️ Displaying balance for default account"); - console.log( - "\n❓ Did you forget to update ETH_KEYSTORE_ACCOUNT=scaffold-eth-custom in the .env file?\n" - ); - } - - console.log(await toString(address, { type: "terminal", small: true })); - console.log("Public address:", address, "\n"); - - await getBalanceForEachNetwork(address); -} - -main().catch((error) => { - console.error(error); - process.exitCode = 1; -}); diff --git a/packages/foundry/scripts-js/generateTsAbis.js b/packages/foundry/scripts-js/generateTsAbis.js deleted file mode 100644 index 274f12c..0000000 --- a/packages/foundry/scripts-js/generateTsAbis.js +++ /dev/null @@ -1,156 +0,0 @@ -import { - readdirSync, - statSync, - readFileSync, - existsSync, - mkdirSync, - writeFileSync, -} from "fs"; -import { join, dirname } from "path"; -import { fileURLToPath } from "url"; -import { format } from "prettier"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const generatedContractComment = ` -/** - * This file is autogenerated by Scaffold-ETH. - * You should not edit it manually or your changes might be overwritten. - */ -`; - -function getDirectories(path) { - return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isDirectory(); - }); -} -function getFiles(path) { - return readdirSync(path).filter(function (file) { - return statSync(path + "/" + file).isFile(); - }); -} -function getArtifactOfContract(contractName) { - const current_path_to_artifacts = join( - __dirname, - "..", - `out/${contractName}.sol` - ); - const artifactJson = JSON.parse( - readFileSync(`${current_path_to_artifacts}/${contractName}.json`) - ); - - return artifactJson; -} - -function getInheritedFromContracts(artifact) { - let inheritedFromContracts = []; - if (artifact?.ast) { - for (const astNode of artifact.ast.nodes) { - if (astNode.nodeType == "ContractDefinition") { - if (astNode.baseContracts.length > 0) { - inheritedFromContracts = astNode.baseContracts.map( - ({ baseName }) => baseName.name - ); - } - } - } - } - return inheritedFromContracts; -} - -function getInheritedFunctions(mainArtifact) { - const inheritedFromContracts = getInheritedFromContracts(mainArtifact); - const inheritedFunctions = {}; - for (const inheritanceContractName of inheritedFromContracts) { - const { - abi, - ast: { absolutePath }, - } = getArtifactOfContract(inheritanceContractName); - for (const abiEntry of abi) { - if (abiEntry.type == "function") { - inheritedFunctions[abiEntry.name] = absolutePath; - } - } - } - return inheritedFunctions; -} - -function main() { - const current_path_to_broadcast = join( - __dirname, - "..", - "broadcast/Deploy.s.sol" - ); - const current_path_to_deployments = join(__dirname, "..", "deployments"); - - const chains = getDirectories(current_path_to_broadcast); - const Deploymentchains = getFiles(current_path_to_deployments); - - const deployments = {}; - - Deploymentchains.forEach((chain) => { - if (!chain.endsWith(".json")) return; - chain = chain.slice(0, -5); - var deploymentObject = JSON.parse( - readFileSync(`${current_path_to_deployments}/${chain}.json`) - ); - deployments[chain] = deploymentObject; - }); - - const allGeneratedContracts = {}; - - chains.forEach((chain) => { - allGeneratedContracts[chain] = {}; - const broadCastObject = JSON.parse( - readFileSync(`${current_path_to_broadcast}/${chain}/run-latest.json`) - ); - const transactionsCreate = broadCastObject.transactions.filter( - (transaction) => transaction.transactionType == "CREATE" - ); - transactionsCreate.forEach((transaction) => { - const artifact = getArtifactOfContract(transaction.contractName); - allGeneratedContracts[chain][ - deployments[chain][transaction.contractAddress] || - transaction.contractName - ] = { - address: transaction.contractAddress, - abi: artifact.abi, - inheritedFunctions: getInheritedFunctions(artifact), - }; - }); - }); - - const TARGET_DIR = "../nextjs/contracts/"; - - const fileContent = Object.entries(allGeneratedContracts).reduce( - (content, [chainId, chainConfig]) => { - return `${content}${parseInt(chainId).toFixed(0)}:${JSON.stringify( - chainConfig, - null, - 2 - )},`; - }, - "" - ); - - if (!existsSync(TARGET_DIR)) { - mkdirSync(TARGET_DIR); - } - writeFileSync( - `${TARGET_DIR}deployedContracts.ts`, - format( - `${generatedContractComment} import { GenericContractsDeclaration } from "~~/utils/scaffold-eth/contract"; \n\n - const deployedContracts = {${fileContent}} as const; \n\n export default deployedContracts satisfies GenericContractsDeclaration`, - { - parser: "typescript", - } - ) - ); -} - -try { - main(); -} catch (error) { - console.error(error); - process.exitCode = 1; -} diff --git a/packages/foundry/src/EtfHook.sol b/packages/foundry/src/EtfHook.sol new file mode 100644 index 0000000..6b24f51 --- /dev/null +++ b/packages/foundry/src/EtfHook.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol"; + +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {ETFManager} from "./EtfToken.sol"; + +contract ETFHook is ETFManager, BaseHook { + address[2] public tokens; // the underlying tokens will be stored in this hook contract + uint256[2] public weights; + uint256 public rebalanceThreshold; + + uint256[2] public tokenBalances; + + constructor( + IPoolManager _poolManager, + address[2] memory _tokens, // only two tokens are supported for now + uint256[2] memory _weights, + uint256 _rebalanceThreshold + ) BaseHook(_poolManager) ETFManager("ETF Token", "ETF") { // TODO: name the ETF token as f"{token0.symbol} + {token1.symbol} ETF" + tokens = _tokens; + weights = _weights; + rebalanceThreshold = _rebalanceThreshold; + for (uint256 i= 0; i < 2; i++) { + tokenBalances[i] = 0; + } + } + + function getHookPermissions() public pure override returns (Hooks.Permissions memory) { + return Hooks.Permissions({ + beforeInitialize: false, + afterInitialize: false, + beforeAddLiquidity: true, + afterAddLiquidity: true, + beforeRemoveLiquidity: true, + afterRemoveLiquidity: false, + beforeSwap: true, + afterSwap: true, + beforeDonate: false, + afterDonate: false, + beforeSwapReturnDelta: false, + afterSwapReturnDelta: false, + afterAddLiquidityReturnDelta: false, + afterRemoveLiquidityReturnDelta: false + }); + } + + function beforeSwap(address sender, PoolKey calldata key, IPoolManager.SwapParams calldata, bytes calldata) + external + override + returns (bytes4, BeforeSwapDelta, uint24) + { + if (checkIfRebalanceNeeded()) { + rebalance(); + } + return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0); + } + + function beforeAddLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + if (checkIfRebalanceNeeded()) { + rebalance(); + } + mintETFToken(0); + return BaseHook.beforeAddLiquidity.selector; + } + + function beforeRemoveLiquidity( + address, + PoolKey calldata key, + IPoolManager.ModifyLiquidityParams calldata, + bytes calldata + ) external override returns (bytes4) { + if (checkIfRebalanceNeeded()) { + rebalance(); + } + burnETFToken(); + return BaseHook.beforeRemoveLiquidity.selector; + } + + // returns each token prices from oracle + function getPrices() public returns (uint256[2] memory prices) { + // TODO: use chainlink, pyth, chronicle + return prices; + } + + function checkIfRebalanceNeeded() private returns (bool) { + uint256[2] memory prices = getPrices(); + + // Calculate current value of each token + uint256[2] memory tokenValues; + for (uint256 i = 0; i < 2; i++) { + tokenValues[i] = prices[i] * tokenBalances[i]; + } + + // Calculate total portfolio value + uint256 totalValue = tokenValues[0] + tokenValues[1]; + if (totalValue == 0) return false; + + // Calculate current weights (in basis points - 10000 = 100%) + uint256[2] memory currentWeights; + for (uint256 i = 0; i < 2; i++) { + currentWeights[i] = (tokenValues[i] * 10000) / totalValue; + } + + // Check if any weight deviates more than the threshold + for (uint256 i = 0; i < 2; i++) { + if (currentWeights[i] > weights[i]) { + if (currentWeights[i] - weights[i] > rebalanceThreshold) return true; + } else { + if (weights[i] - currentWeights[i] > rebalanceThreshold) return true; + } + } + + return false; + } + + function rebalance() private { + uint256[2] memory prices = getPrices(); + + // Calculate current value of each token + uint256[2] memory tokenValues; + for (uint256 i = 0; i < 2; i++) { + tokenValues[i] = prices[i] * tokenBalances[i]; + } + + // Calculate total portfolio value + uint256 totalValue = tokenValues[0] + tokenValues[1]; + if (totalValue == 0) return; + + // Calculate target values for each token + uint256[2] memory targetValues; + for (uint256 i = 0; i < 2; i++) { + targetValues[i] = (totalValue * weights[i]) / 10000; + } + + // Determine which token to sell and which to buy + if (tokenValues[0] > targetValues[0]) { + // Token 0 is overweight, sell token 0 for token 1 + uint256 token0ToSell = (tokenValues[0] - targetValues[0]) / prices[0]; + // Execute swap through Uniswap pool + // TODO: Implement swap logic using poolManager + } else { + // Token 1 is overweight, sell token 1 for token 0 + uint256 token1ToSell = (tokenValues[1] - targetValues[1]) / prices[1]; + // Execute swap through Uniswap pool + // TODO: Implement swap logic using poolManager + } + } + + function mintETFToken(uint256 etfAmount) private { + // transfer tokens to ETF pool contract + // update token balances + // mint ETF token to msg.sender + } + + function burnETFToken() private { + // transfer tokens to msg.sender + // update token balances + // burn ETF token from msg.sender + } +} \ No newline at end of file diff --git a/packages/foundry/src/EtfToken.sol b/packages/foundry/src/EtfToken.sol new file mode 100644 index 0000000..9683f54 --- /dev/null +++ b/packages/foundry/src/EtfToken.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ERC20} from "solmate/src/tokens/ERC20.sol"; +import {Owned} from "solmate/src/auth/Owned.sol"; + +contract ETFToken is ERC20, Owned { + constructor (string memory name, string memory symbol) ERC20(name, symbol, 18) Owned(msg.sender) {} + + function mint(address to, uint256 amount) external onlyOwner { + _mint(to, amount); + } + + function burn(address from, uint256 amount) external onlyOwner { + _burn(from, amount); + } +} + +contract ETFManager { + ETFToken public etfToken; // the ETF token will be minted and burned in this hook contract + + constructor(string memory name, string memory symbol) { + etfToken = new ETFToken(name, symbol); + } + + function mint(address to, uint256 amount) internal { + etfToken.mint(to, amount); + } + + function burn(address from, uint256 amount) internal { + etfToken.burn(from, amount); + } +} \ No newline at end of file diff --git a/packages/foundry/test/Counter.t.sol b/packages/foundry/test/Counter.t.sol new file mode 100644 index 0000000..0ca7c97 --- /dev/null +++ b/packages/foundry/test/Counter.t.sol @@ -0,0 +1,124 @@ +// // SPDX-License-Identifier: MIT +// pragma solidity ^0.8.24; + +// import "forge-std/Test.sol"; +// import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +// import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +// import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +// import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +// import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +// import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +// import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +// import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +// import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; +// import {Counter} from "../src/Counter.sol"; +// import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; + +// import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; +// import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +// import {EasyPosm} from "./utils/EasyPosm.sol"; +// import {Fixtures} from "./utils/Fixtures.sol"; + +// contract CounterTest is Test, Fixtures { +// using EasyPosm for IPositionManager; +// using PoolIdLibrary for PoolKey; +// using CurrencyLibrary for Currency; +// using StateLibrary for IPoolManager; + +// Counter hook; +// PoolId poolId; + +// uint256 tokenId; +// int24 tickLower; +// int24 tickUpper; + +// function setUp() public { +// // creates the pool manager, utility routers, and test tokens +// deployFreshManagerAndRouters(); +// deployMintAndApprove2Currencies(); + +// deployAndApprovePosm(manager); + +// // Deploy the hook to an address with the correct flags +// address flags = address( +// uint160( +// Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG +// | Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG +// ) ^ (0x4444 << 144) // Namespace the hook to avoid collisions +// ); +// bytes memory constructorArgs = abi.encode(manager); //Add all the necessary constructor arguments from the hook +// deployCodeTo("Counter.sol:Counter", constructorArgs, flags); +// hook = Counter(flags); + +// // Create the pool +// key = PoolKey(currency0, currency1, 3000, 60, IHooks(hook)); +// poolId = key.toId(); +// manager.initialize(key, SQRT_PRICE_1_1); + +// // Provide full-range liquidity to the pool +// tickLower = TickMath.minUsableTick(key.tickSpacing); +// tickUpper = TickMath.maxUsableTick(key.tickSpacing); + +// uint128 liquidityAmount = 100e18; + +// (uint256 amount0Expected, uint256 amount1Expected) = LiquidityAmounts.getAmountsForLiquidity( +// SQRT_PRICE_1_1, +// TickMath.getSqrtPriceAtTick(tickLower), +// TickMath.getSqrtPriceAtTick(tickUpper), +// liquidityAmount +// ); + +// (tokenId,) = posm.mint( +// key, +// tickLower, +// tickUpper, +// liquidityAmount, +// amount0Expected + 1, +// amount1Expected + 1, +// address(this), +// block.timestamp, +// ZERO_BYTES +// ); +// } + +// function testCounterHooks() public { +// // positions were created in setup() +// assertEq(hook.beforeAddLiquidityCount(poolId), 1); +// assertEq(hook.beforeRemoveLiquidityCount(poolId), 0); + +// assertEq(hook.beforeSwapCount(poolId), 0); +// assertEq(hook.afterSwapCount(poolId), 0); + +// // Perform a test swap // +// bool zeroForOne = true; +// int256 amountSpecified = -1e18; // negative number indicates exact input swap! +// BalanceDelta swapDelta = swap(key, zeroForOne, amountSpecified, ZERO_BYTES); +// // ------------------- // + +// assertEq(int256(swapDelta.amount0()), amountSpecified); + +// assertEq(hook.beforeSwapCount(poolId), 1); +// assertEq(hook.afterSwapCount(poolId), 1); +// } + +// function testLiquidityHooks() public { +// // positions were created in setup() +// assertEq(hook.beforeAddLiquidityCount(poolId), 1); +// assertEq(hook.beforeRemoveLiquidityCount(poolId), 0); + +// // remove liquidity +// uint256 liquidityToRemove = 1e18; +// posm.decreaseLiquidity( +// tokenId, +// liquidityToRemove, +// MAX_SLIPPAGE_REMOVE_LIQUIDITY, +// MAX_SLIPPAGE_REMOVE_LIQUIDITY, +// address(this), +// block.timestamp, +// ZERO_BYTES +// ); + +// assertEq(hook.beforeAddLiquidityCount(poolId), 1); +// assertEq(hook.beforeRemoveLiquidityCount(poolId), 1); +// } +// } diff --git a/packages/foundry/test/YourContract.t.sol b/packages/foundry/test/YourContract.t.sol deleted file mode 100644 index 5104250..0000000 --- a/packages/foundry/test/YourContract.t.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "forge-std/Test.sol"; -// import "../contracts/YourContract.sol"; - -contract YourContractTest is Test { -/* YourContract public yourContract; - - function setUp() public { - yourContract = new YourContract(vm.addr(1)); - } - - function testMessageOnDeployment() public view { - require( - keccak256(bytes(yourContract.greeting())) - == keccak256("Building Unstoppable Apps!!!") - ); - } */ -} diff --git a/packages/foundry/test/custom-accounting/ExampleHook.sol b/packages/foundry/test/custom-accounting/ExampleHook.sol new file mode 100644 index 0000000..1c5fd1b --- /dev/null +++ b/packages/foundry/test/custom-accounting/ExampleHook.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {BeforeSwapDelta, toBeforeSwapDelta} from "v4-core/src/types/BeforeSwapDelta.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; +import {CurrencySettler} from "v4-core/test/utils/CurrencySettler.sol"; +import {BaseTestHooks} from "v4-core/src/test/BaseTestHooks.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; + +contract ExampleHook is BaseTestHooks { + using Hooks for IHooks; + using CurrencySettler for Currency; + + IPoolManager immutable manager; + + constructor(IPoolManager _manager) { + manager = _manager; + } + + modifier onlyPoolManager() { + require(msg.sender == address(manager)); + _; + } + + function beforeSwap( + address, /* sender **/ + PoolKey calldata key, + IPoolManager.SwapParams calldata params, + bytes calldata /* hookData **/ + ) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { + (Currency inputCurrency, Currency outputCurrency, uint256 amount) = _getInputOutputAndAmount(key, params); + + // this "custom curve" is a line, 1-1 + // take the full input amount, and give the full output amount + manager.take(inputCurrency, address(this), amount); + + outputCurrency.settle(manager, address(this), amount, false); + + // return -amountSpecified as specified to no-op the concentrated liquidity swap + BeforeSwapDelta hookDelta = toBeforeSwapDelta(int128(-params.amountSpecified), int128(params.amountSpecified)); + return (IHooks.beforeSwap.selector, hookDelta, 0); + } + + function _getInputOutputAndAmount(PoolKey calldata key, IPoolManager.SwapParams calldata params) + internal + pure + returns (Currency input, Currency output, uint256 amount) + { + (input, output) = params.zeroForOne ? (key.currency0, key.currency1) : (key.currency1, key.currency0); + + amount = params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified); + } +} diff --git a/packages/foundry/test/custom-accounting/ExampleHook.t.sol b/packages/foundry/test/custom-accounting/ExampleHook.t.sol new file mode 100644 index 0000000..f1d6452 --- /dev/null +++ b/packages/foundry/test/custom-accounting/ExampleHook.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Deployers} from "v4-core/test/utils/Deployers.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {Currency} from "v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {SafeCast} from "v4-core/src/libraries/SafeCast.sol"; +import {Constants} from "v4-core/test/utils/Constants.sol"; +import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol"; +import {ExampleHook} from "./ExampleHook.sol"; + +import "forge-std/console2.sol"; + +contract ExampleHookTest is Test, Deployers { + using SafeCast for *; + + // TODO: Initialize this test with your hook. You will pass in your hook implementation before each test to set this. + address hook; + address user = address(0xBEEF); + + function setUp() public { + initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0))); + } + + function test_exampleHook_beforeSwap() public { + // TODO: This is where you pass in your hook's implementation. + address impl = address(new ExampleHook(manager)); + _setUpBeforeSwapHook(impl); + + _setApprovalsFor(user, address(Currency.unwrap(key.currency0))); + _setApprovalsFor(user, address(Currency.unwrap(key.currency1))); + + // Seeds liquidity into the hook. + key.currency0.transfer(address(hook), 10e18); + key.currency1.transfer(address(hook), 10e18); + + // Seeds liquidity into the user. + + key.currency0.transfer(address(user), 10e18); + key.currency1.transfer(address(user), 10e18); + + // Seed liquidity into the user. + + uint256 userBalanceBefore0 = currency0.balanceOf(address(user)); + uint256 userBalanceBefore1 = currency1.balanceOf(address(user)); + + uint256 hookBalanceBefore0 = currency0.balanceOf(address(hook)); + uint256 hookBalanceBefore1 = currency1.balanceOf(address(hook)); + + // TODO: Change swap amount. Note: Remember that if a hook is taking from the pool based on this amount, the pool must have at least this amount of liquidity. + uint256 amountToSwap = 1e6; + + // TODO: Change depending on what kind of swap you want. + // Setting this value to true means currency0 is supplied. + // Setting this value to false means currency1 is supplied. + bool zeroForOne = false; + + // TODO: Set the sign of this value. + // A negative amount means it is an exactInput swap, so the user is sending exactly that amount into the pool. + // A positive amount means it is an exactOutput swap, so the user is only requesting that amount out of the swap. + int256 amountSpecified = int256(amountToSwap); + + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + // Note: if zeroForOne is true, the price is pushed down, otherwise its pushed up. + sqrtPriceLimitX96: zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT + }); + + _printTestType(params.zeroForOne, params.amountSpecified); + + console2.log("--- STARTING BALANCES ---"); + + console2.log("User balance in currency0 before swapping: ", userBalanceBefore0); + console2.log("User balance in currency1 before swapping: ", userBalanceBefore1); + console2.log("Hook balance in currency0 before swapping: ", hookBalanceBefore0); + console2.log("Hook balance in currency1 before swapping: ", hookBalanceBefore1); + + vm.prank(user); + swapRouter.swap(key, params, _defaultTestSettings(), ZERO_BYTES); + + uint256 userBalanceAfter0 = currency0.balanceOf(address(user)); + uint256 userBalanceAfter1 = currency1.balanceOf(address(user)); + + uint256 hookBalanceAfter0 = currency0.balanceOf(address(hook)); + uint256 hookBalanceAfter1 = currency1.balanceOf(address(hook)); + + console2.log("--- ENDING BALANCES ---"); + + console2.log("User balance in currency0 after swapping: ", userBalanceAfter0); + console2.log("User balance in currency1 after swapping: ", userBalanceAfter1); + console2.log("Hook balance in currency0 after swapping: ", hookBalanceAfter0); + console2.log("Hook balance in currency1 after swapping: ", hookBalanceAfter1); + + if (zeroForOne) { + assertEq(userBalanceAfter0, userBalanceBefore0 - amountToSwap, "amount 0"); + assertEq(userBalanceAfter1, userBalanceBefore1 + amountToSwap, "amount 1"); + } else { + assertEq(userBalanceAfter0, userBalanceBefore0 + amountToSwap, "amount 0"); + assertEq(userBalanceAfter1, userBalanceBefore1 - amountToSwap, "amount 1"); + } + } + + /// INTERNAL HELPER FUNCTIONS /// + + function _printTestType(bool zeroForOne, int256 amountSpecified) internal { + console2.log("--- TEST TYPE ---"); + string memory zeroForOneString = zeroForOne ? "zeroForOne" : "oneForZero"; + string memory swapType = amountSpecified < 0 ? "exactInput" : "exactOutput"; + string memory currencyRequiredFromUser = zeroForOne ? "currency0" : "currency1"; + string memory currencySpecified = zeroForOne == amountSpecified < 0 ? "currency0" : "currency1"; + + console2.log("This is a", zeroForOneString, swapType, "swap"); + console2.log("The user will owe an amount in", currencyRequiredFromUser); + console2.log("The currency specified is", currencySpecified); + } + + function _setUpBeforeSwapHook(address impl) internal { + address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); + _etchHookAndInitPool(hookAddr, impl); + } + + function _etchHookAndInitPool(address hookAddr, address implAddr) internal { + vm.etch(hookAddr, implAddr.code); + hook = hookAddr; + (key,) = initPoolAndAddLiquidity(currency0, currency1, IHooks(hook), 100, SQRT_PRICE_1_1); + } + + function _defaultTestSettings() internal returns (PoolSwapTest.TestSettings memory testSetting) { + return PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + } + + function _setApprovalsFor(address _user, address token) internal { + address[8] memory toApprove = [ + address(swapRouter), + address(swapRouterNoChecks), + address(modifyLiquidityRouter), + address(modifyLiquidityNoChecks), + address(donateRouter), + address(takeRouter), + address(claimsRouter), + address(nestedActionRouter.executor()) + ]; + + for (uint256 i = 0; i < toApprove.length; i++) { + vm.prank(_user); + MockERC20(token).approve(toApprove[i], Constants.MAX_UINT256); + } + } +} diff --git a/packages/foundry/test/utils/EasyPosm.sol b/packages/foundry/test/utils/EasyPosm.sol new file mode 100644 index 0000000..a04bb1b --- /dev/null +++ b/packages/foundry/test/utils/EasyPosm.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.21; + +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {BalanceDelta, toBalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {Currency, CurrencyLibrary} from "v4-core/src/types/Currency.sol"; +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +import {Actions} from "v4-periphery/src/libraries/Actions.sol"; +import {SafeCast} from "v4-core/src/libraries/SafeCast.sol"; +import {PositionInfo, PositionInfoLibrary} from "v4-periphery/src/libraries/PositionInfoLibrary.sol"; + +/// @title Easy Position Manager +/// @notice A library for abstracting Position Manager calldata +/// @dev Useable onchain, but expensive because of encoding +library EasyPosm { + using CurrencyLibrary for Currency; + using SafeCast for uint256; + using SafeCast for int256; + using PositionInfoLibrary for PositionInfo; + + /// @dev packing data to avoid stack too deep error + struct MintData { + uint256 balance0Before; + uint256 balance1Before; + bytes[] params; + } + + /// @dev This function supports sending native tokens (ETH), the amount-to-pay is determined by amount0Max. + /// Any excess amount is NOT refunded since it is not encoding the SWEEP action + function mint( + IPositionManager posm, + PoolKey memory poolKey, + int24 tickLower, + int24 tickUpper, + uint256 liquidity, + uint256 amount0Max, + uint256 amount1Max, + address recipient, + uint256 deadline, + bytes memory hookData + ) internal returns (uint256 tokenId, BalanceDelta delta) { + (Currency currency0, Currency currency1) = (poolKey.currency0, poolKey.currency1); + + MintData memory mintData = MintData({ + balance0Before: currency0.balanceOf(address(this)), + balance1Before: currency1.balanceOf(address(this)), + params: new bytes[](2) + }); + mintData.params[0] = + abi.encode(poolKey, tickLower, tickUpper, liquidity, amount0Max, amount1Max, recipient, hookData); + mintData.params[1] = abi.encode(currency0, currency1); + + // Mint Liquidity + tokenId = posm.nextTokenId(); + uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; + posm.modifyLiquidities{value: valueToPass}( + abi.encode(abi.encodePacked(uint8(Actions.MINT_POSITION), uint8(Actions.SETTLE_PAIR)), mintData.params), + deadline + ); + + delta = toBalanceDelta( + -(mintData.balance0Before - currency0.balanceOf(address(this))).toInt128(), + -(mintData.balance1Before - currency1.balanceOf(address(this))).toInt128() + ); + } + + function increaseLiquidity( + IPositionManager posm, + uint256 tokenId, + uint256 liquidityToAdd, + uint256 amount0Max, + uint256 amount1Max, + uint256 deadline, + bytes memory hookData + ) internal returns (BalanceDelta delta) { + (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId); + + bytes[] memory params = new bytes[](3); + params[0] = abi.encode(tokenId, liquidityToAdd, amount0Max, amount1Max, hookData); + params[1] = abi.encode(currency0); + params[2] = abi.encode(currency1); + + uint256 balance0Before = currency0.balanceOf(address(this)); + uint256 balance1Before = currency1.balanceOf(address(this)); + + uint256 valueToPass = currency0.isAddressZero() ? amount0Max : 0; + posm.modifyLiquidities{value: valueToPass}( + abi.encode( + abi.encodePacked( + uint8(Actions.INCREASE_LIQUIDITY), uint8(Actions.CLOSE_CURRENCY), uint8(Actions.CLOSE_CURRENCY) + ), + params + ), + deadline + ); + + delta = toBalanceDelta( + (currency0.balanceOf(address(this)).toInt256() - balance0Before.toInt256()).toInt128(), + (currency1.balanceOf(address(this)).toInt256() - balance1Before.toInt256()).toInt128() + ); + } + + function decreaseLiquidity( + IPositionManager posm, + uint256 tokenId, + uint256 liquidityToRemove, + uint256 amount0Min, + uint256 amount1Min, + address recipient, + uint256 deadline, + bytes memory hookData + ) internal returns (BalanceDelta delta) { + (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId); + + bytes[] memory params = new bytes[](2); + params[0] = abi.encode(tokenId, liquidityToRemove, amount0Min, amount1Min, hookData); + params[1] = abi.encode(currency0, currency1, recipient); + + uint256 balance0Before = currency0.balanceOf(address(this)); + uint256 balance1Before = currency1.balanceOf(address(this)); + + posm.modifyLiquidities( + abi.encode(abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)), params), deadline + ); + + delta = toBalanceDelta( + (currency0.balanceOf(address(this)) - balance0Before).toInt128(), + (currency1.balanceOf(address(this)) - balance1Before).toInt128() + ); + } + + function collect( + IPositionManager posm, + uint256 tokenId, + uint256 amount0Min, + uint256 amount1Min, + address recipient, + uint256 deadline, + bytes memory hookData + ) internal returns (BalanceDelta delta) { + (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId); + + bytes[] memory params = new bytes[](2); + // collecting fees is achieved by decreasing liquidity with 0 liquidity removed + params[0] = abi.encode(tokenId, 0, amount0Min, amount1Min, hookData); + params[1] = abi.encode(currency0, currency1, recipient); + + uint256 balance0Before = currency0.balanceOf(recipient); + uint256 balance1Before = currency1.balanceOf(recipient); + + posm.modifyLiquidities( + abi.encode(abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR)), params), deadline + ); + + delta = toBalanceDelta( + (currency0.balanceOf(recipient) - balance0Before).toInt128(), + (currency1.balanceOf(recipient) - balance1Before).toInt128() + ); + } + + function burn( + IPositionManager posm, + uint256 tokenId, + uint256 amount0Min, + uint256 amount1Min, + address recipient, + uint256 deadline, + bytes memory hookData + ) internal returns (BalanceDelta delta) { + (Currency currency0, Currency currency1) = getCurrencies(posm, tokenId); + + bytes[] memory params = new bytes[](2); + params[0] = abi.encode(tokenId, 0, amount0Min, amount1Min, hookData); + params[1] = abi.encode(currency0, currency1, recipient); + + uint256 balance0Before = currency0.balanceOf(recipient); + uint256 balance1Before = currency1.balanceOf(recipient); + + posm.modifyLiquidities( + abi.encode(abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR)), params), deadline + ); + + delta = toBalanceDelta( + (currency0.balanceOf(recipient) - balance0Before).toInt128(), + (currency1.balanceOf(recipient) - balance1Before).toInt128() + ); + } + + function getCurrencies(IPositionManager posm, uint256 tokenId) + internal + view + returns (Currency currency0, Currency currency1) + { + (PoolKey memory key,) = posm.getPoolAndPositionInfo(tokenId); + return (key.currency0, key.currency1); + } +} diff --git a/packages/foundry/test/utils/EasyPosm.t.sol b/packages/foundry/test/utils/EasyPosm.t.sol new file mode 100644 index 0000000..f454111 --- /dev/null +++ b/packages/foundry/test/utils/EasyPosm.t.sol @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/Test.sol"; +import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {TickMath} from "v4-core/src/libraries/TickMath.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; +import {CurrencyLibrary, Currency} from "v4-core/src/types/Currency.sol"; +import {StateLibrary} from "v4-core/src/libraries/StateLibrary.sol"; +import {LiquidityAmounts} from "v4-core/test/utils/LiquidityAmounts.sol"; + +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +import {EasyPosm} from "./EasyPosm.sol"; +import {Fixtures} from "./Fixtures.sol"; + +contract CounterTest is Test, Fixtures { + using EasyPosm for IPositionManager; + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + using StateLibrary for IPoolManager; + + int24 tickLower; + int24 tickUpper; + + function setUp() public { + // creates the pool manager, utility routers, and test tokens + deployFreshManagerAndRouters(); + deployMintAndApprove2Currencies(); + + deployAndApprovePosm(manager); + + // Create the pool + key = PoolKey(currency0, currency1, 3000, 60, IHooks(address(0))); + nativeKey = PoolKey(Currency.wrap(address(0)), currency1, 3000, 60, IHooks(address(0))); + + manager.initialize(key, SQRT_PRICE_1_1); + manager.initialize(nativeKey, SQRT_PRICE_1_1); + + // full-range liquidity + tickLower = TickMath.minUsableTick(key.tickSpacing); + tickUpper = TickMath.maxUsableTick(key.tickSpacing); + } + + function test_mintLiquidity() public { + uint256 liquidityToMint = 100e18; + address recipient = address(this); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToMint) + ); + + (, BalanceDelta delta) = posm.mint( + key, + tickLower, + tickUpper, + liquidityToMint, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + recipient, + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei))); + assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei))); + } + + function test_mintLiquidityNative() public { + uint256 liquidityToMint = 100e18; + address recipient = address(this); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToMint) + ); + + vm.deal(address(this), amount0 + 1); + (, BalanceDelta delta) = posm.mint( + nativeKey, + tickLower, + tickUpper, + liquidityToMint, + amount0 + 1, + amount1 + 1, + recipient, + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei))); + assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei))); + } + + function test_increaseLiquidity() public { + (uint256 tokenId,) = posm.mint( + key, + tickLower, + tickUpper, + 100e18, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + + uint256 liquidityToAdd = 1e18; + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToAdd) + ); + + BalanceDelta delta = posm.increaseLiquidity( + tokenId, + liquidityToAdd, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei))); + assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei))); + } + + function test_increaseLiquidityNative() public { + uint256 liquidityToMint = 100e18; + address recipient = address(this); + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToMint) + ); + + vm.deal(address(this), amount0 + 1); + (uint256 tokenId, BalanceDelta delta) = posm.mint( + nativeKey, + tickLower, + tickUpper, + liquidityToMint, + amount0 + 1, + amount1 + 1, + recipient, + block.timestamp + 1, + ZERO_BYTES + ); + + uint256 liquidityToIncrease = 1e18; + + (amount0, amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToIncrease) + ); + + vm.deal(address(this), amount0 + 1); + delta = posm.increaseLiquidity( + tokenId, liquidityToIncrease, amount0 + 1, amount1 + 1, block.timestamp + 1, ZERO_BYTES + ); + assertEq(delta.amount0(), -int128(uint128(amount0 + 1 wei))); + assertEq(delta.amount1(), -int128(uint128(amount1 + 1 wei))); + } + + function test_decreaseLiquidity() public { + (uint256 tokenId,) = posm.mint( + key, + tickLower, + tickUpper, + 100e18, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + + uint256 liquidityToRemove = 1e18; + + (uint256 amount0, uint256 amount1) = LiquidityAmounts.getAmountsForLiquidity( + SQRT_PRICE_1_1, + TickMath.getSqrtPriceAtTick(tickLower), + TickMath.getSqrtPriceAtTick(tickUpper), + uint128(liquidityToRemove) + ); + + BalanceDelta delta = posm.decreaseLiquidity( + tokenId, + liquidityToRemove, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(delta.amount0(), int128(uint128(amount0))); + assertEq(delta.amount1(), int128(uint128(amount1))); + } + + function test_burn() public { + (uint256 tokenId, BalanceDelta mintDelta) = posm.mint( + key, + tickLower, + tickUpper, + 100e18, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + + BalanceDelta delta = posm.burn( + tokenId, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(delta.amount0(), -mintDelta.amount0() - 1 wei); + assertEq(delta.amount1(), -mintDelta.amount1() - 1 wei); + } + + function test_collect() public { + (uint256 tokenId,) = posm.mint( + key, + tickLower, + tickUpper, + 100e18, + MAX_SLIPPAGE_ADD_LIQUIDITY, + MAX_SLIPPAGE_ADD_LIQUIDITY, + address(this), + block.timestamp + 1, + ZERO_BYTES + ); + + // donate to regenerate fee revenue + uint128 feeRevenue0 = 1e18; + uint128 feeRevenue1 = 0.1e18; + donateRouter.donate(key, feeRevenue0, feeRevenue1, ZERO_BYTES); + + // position collects half of the revenue since 50% of the liquidity is minted in setUp() + BalanceDelta delta = posm.collect( + tokenId, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + MAX_SLIPPAGE_REMOVE_LIQUIDITY, + address(0x123), + block.timestamp + 1, + ZERO_BYTES + ); + assertEq(uint128(delta.amount0()), feeRevenue0 - 1 wei); + assertEq(uint128(delta.amount1()), feeRevenue1 - 1 wei); + } +} diff --git a/packages/foundry/test/utils/Fixtures.sol b/packages/foundry/test/utils/Fixtures.sol new file mode 100644 index 0000000..ca61acc --- /dev/null +++ b/packages/foundry/test/utils/Fixtures.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Currency} from "v4-core/src/types/Currency.sol"; +import {BalanceDelta} from "v4-core/src/types/BalanceDelta.sol"; +import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; +import {PositionManager} from "v4-periphery/src/PositionManager.sol"; +import {IPositionManager} from "v4-periphery/src/interfaces/IPositionManager.sol"; +import {Hooks} from "v4-core/src/libraries/Hooks.sol"; +import {PoolKey} from "v4-core/src/types/PoolKey.sol"; +import {Deployers} from "v4-core/test/utils/Deployers.sol"; +import {IERC20} from "forge-std/interfaces/IERC20.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; +import {DeployPermit2} from "./forks/DeployPermit2.sol"; +import {IERC721Permit_v4} from "v4-periphery/src/interfaces/IERC721Permit_v4.sol"; +import {IEIP712_v4} from "v4-periphery/src/interfaces/IEIP712_v4.sol"; +import {ERC721PermitHash} from "v4-periphery/src/libraries/ERC721PermitHash.sol"; +import {IPositionDescriptor} from "v4-periphery/src/interfaces/IPositionDescriptor.sol"; +import {IWETH9} from "v4-periphery/src/interfaces/external/IWETH9.sol"; + +/// @notice A shared test contract that wraps the v4-core deployers contract and exposes basic liquidity operations on posm. +contract Fixtures is Deployers, DeployPermit2 { + uint256 constant STARTING_USER_BALANCE = 10_000_000 ether; + uint256 constant MAX_SLIPPAGE_ADD_LIQUIDITY = type(uint256).max; + uint256 constant MAX_SLIPPAGE_REMOVE_LIQUIDITY = 0; + + IPositionManager posm; + + function deployAndApprovePosm(IPoolManager poolManager) public { + deployPosm(poolManager); + approvePosm(); + } + + function deployPosm(IPoolManager poolManager) internal { + // We use vm.etch to prevent having to use via-ir in this repository. + etchPermit2(); + posm = IPositionManager(new PositionManager(poolManager, permit2, 300_000, IPositionDescriptor(address(0)), IWETH9(address(0)))); + } + + function seedBalance(address to) internal { + IERC20(Currency.unwrap(currency0)).transfer(to, STARTING_USER_BALANCE); + IERC20(Currency.unwrap(currency1)).transfer(to, STARTING_USER_BALANCE); + } + + function approvePosm() internal { + approvePosmCurrency(currency0); + approvePosmCurrency(currency1); + } + + function approvePosmCurrency(Currency currency) internal { + // Because POSM uses permit2, we must execute 2 permits/approvals. + // 1. First, the caller must approve permit2 on the token. + IERC20(Currency.unwrap(currency)).approve(address(permit2), type(uint256).max); + // 2. Then, the caller must approve POSM as a spender of permit2. TODO: This could also be a signature. + permit2.approve(Currency.unwrap(currency), address(posm), type(uint160).max, type(uint48).max); + } + + // Does the same approvals as approvePosm, but for a specific address. + function approvePosmFor(address addr) internal { + vm.startPrank(addr); + approvePosm(); + vm.stopPrank(); + } + + function permit(uint256 privateKey, uint256 tokenId, address operator, uint256 nonce) internal { + bytes32 digest = getDigest(operator, tokenId, 1, block.timestamp + 1); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, digest); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.prank(operator); + IERC721Permit_v4(address(posm)).permit(operator, tokenId, block.timestamp + 1, nonce, signature); + } + + function getDigest(address spender, uint256 tokenId, uint256 nonce, uint256 deadline) + internal + view + returns (bytes32 digest) + { + digest = keccak256( + abi.encodePacked( + "\x19\x01", + IEIP712_v4(address(posm)).DOMAIN_SEPARATOR(), + keccak256(abi.encode(ERC721PermitHash.PERMIT_TYPEHASH, spender, tokenId, nonce, deadline)) + ) + ); + } +} diff --git a/packages/foundry/test/utils/HookMiner.sol b/packages/foundry/test/utils/HookMiner.sol new file mode 100644 index 0000000..d6b30c4 --- /dev/null +++ b/packages/foundry/test/utils/HookMiner.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.21; + +/// @title HookMiner - a library for mining hook addresses +/// @dev This library is intended for `forge test` environments. There may be gotchas when using salts in `forge script` or `forge create` +library HookMiner { + // mask to slice out the bottom 14 bit of the address + uint160 constant FLAG_MASK = 0x3FFF; + + // Maximum number of iterations to find a salt, avoid infinite loops + uint256 constant MAX_LOOP = 100_000; + + /// @notice Find a salt that produces a hook address with the desired `flags` + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param flags The desired flags for the hook address + /// @param creationCode The creation code of a hook contract. Example: `type(Counter).creationCode` + /// @param constructorArgs The encoded constructor arguments of a hook contract. Example: `abi.encode(address(manager))` + /// @return hookAddress salt and corresponding address that was found. The salt can be used in `new Hook{salt: salt}()` + function find(address deployer, uint160 flags, bytes memory creationCode, bytes memory constructorArgs) + internal + view + returns (address, bytes32) + { + address hookAddress; + bytes memory creationCodeWithArgs = abi.encodePacked(creationCode, constructorArgs); + + uint256 salt; + for (salt; salt < MAX_LOOP; salt++) { + hookAddress = computeAddress(deployer, salt, creationCodeWithArgs); + if (uint160(hookAddress) & FLAG_MASK == flags && hookAddress.code.length == 0) { + return (hookAddress, bytes32(salt)); + } + } + revert("HookMiner: could not find salt"); + } + + /// @notice Precompute a contract address deployed via CREATE2 + /// @param deployer The address that will deploy the hook. In `forge test`, this will be the test contract `address(this)` or the pranking address + /// In `forge script`, this should be `0x4e59b44847b379578588920cA78FbF26c0B4956C` (CREATE2 Deployer Proxy) + /// @param salt The salt used to deploy the hook + /// @param creationCode The creation code of a hook contract + function computeAddress(address deployer, uint256 salt, bytes memory creationCode) + internal + pure + returns (address hookAddress) + { + return address( + uint160(uint256(keccak256(abi.encodePacked(bytes1(0xFF), deployer, salt, keccak256(creationCode))))) + ); + } +} diff --git a/packages/foundry/test/utils/forks/DeployPermit2.sol b/packages/foundry/test/utils/forks/DeployPermit2.sol new file mode 100644 index 0000000..70165fc --- /dev/null +++ b/packages/foundry/test/utils/forks/DeployPermit2.sol @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {CommonBase} from "forge-std/Base.sol"; +import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol"; + +import {Permit2Bytecode} from "./Permit2Bytecode.sol"; + +/// @notice helper to deploy permit2 from precompiled bytecode. To be used in foundry tests and scripts +/// @dev useful if testing externally against permit2 and want to avoid +/// recompiling entirely and requiring viaIR compilation +/// a fork of DeployPermit2 from the permit2 repository +contract DeployPermit2 is CommonBase, Permit2Bytecode { + IAllowanceTransfer permit2 = IAllowanceTransfer(address(0x000000000022D473030F116dDEE9F6B43aC78BA3)); + + /// @notice Deploys permit2 with vm.etch, to be used in foundry tests + function etchPermit2() public returns (IAllowanceTransfer) { + vm.etch(address(permit2), PERMIT2_BYTECODE); + return permit2; + } + + /// @notice Deploys permit2 with anvil_setCode, to be used in foundry scripts against anvil + function anvilPermit2() public returns (IAllowanceTransfer) { + vm.rpc( + "anvil_setCode", + string.concat('["', vm.toString(address(permit2)), '","', vm.toString(PERMIT2_BYTECODE), '"]') + ); + return permit2; + } +} diff --git a/packages/foundry/test/utils/forks/Permit2Bytecode.sol b/packages/foundry/test/utils/forks/Permit2Bytecode.sol new file mode 100644 index 0000000..89cc95e --- /dev/null +++ b/packages/foundry/test/utils/forks/Permit2Bytecode.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract Permit2Bytecode { + bytes constant PERMIT2_BYTECODE = + hex"6040608081526004908136101561001557600080fd5b600090813560e01c80630d58b1db1461126c578063137c29fe146110755780632a2d80d114610db75780632b67b57014610bde57806330f28b7a14610ade5780633644e51514610a9d57806336c7851614610a285780633ff9dcb1146109a85780634fe02b441461093f57806365d9723c146107ac57806387517c451461067a578063927da105146105c3578063cc53287f146104a3578063edd9444b1461033a5763fe8ec1a7146100c657600080fd5b346103365760c07ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff833581811161033257610114903690860161164b565b60243582811161032e5761012b903690870161161a565b6101336114e6565b9160843585811161032a5761014b9036908a016115c1565b98909560a43590811161032657610164913691016115c1565b969095815190610173826113ff565b606b82527f5065726d697442617463685769746e6573735472616e7366657246726f6d285460208301527f6f6b656e5065726d697373696f6e735b5d207065726d69747465642c61646472838301527f657373207370656e6465722c75696e74323536206e6f6e63652c75696e74323560608301527f3620646561646c696e652c000000000000000000000000000000000000000000608083015282519a8b9181610222602085018096611f93565b918237018a8152039961025b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe09b8c8101835282611437565b5190209085515161026b81611ebb565b908a5b8181106102f95750506102f6999a6102ed9183516102a081610294602082018095611f66565b03848101835282611437565b519020602089810151858b015195519182019687526040820192909252336060820152608081019190915260a081019390935260643560c08401528260e081015b03908101835282611437565b51902093611cf7565b80f35b8061031161030b610321938c5161175e565b51612054565b61031b828661175e565b52611f0a565b61026e565b8880fd5b8780fd5b8480fd5b8380fd5b5080fd5b5091346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103365767ffffffffffffffff9080358281116103325761038b903690830161164b565b60243583811161032e576103a2903690840161161a565b9390926103ad6114e6565b9160643590811161049f576103c4913691016115c1565b949093835151976103d489611ebb565b98885b81811061047d5750506102f697988151610425816103f9602082018095611f66565b037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe08101835282611437565b5190206020860151828701519083519260208401947ffcf35f5ac6a2c28868dc44c302166470266239195f02b0ee408334829333b7668652840152336060840152608083015260a082015260a081526102ed8161141b565b808b61031b8261049461030b61049a968d5161175e565b9261175e565b6103d7565b8680fd5b5082346105bf57602090817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126103325780359067ffffffffffffffff821161032e576104f49136910161161a565b929091845b848110610504578580f35b8061051a610515600193888861196c565b61197c565b61052f84610529848a8a61196c565b0161197c565b3389528385528589209173ffffffffffffffffffffffffffffffffffffffff80911692838b528652868a20911690818a5285528589207fffffffffffffffffffffffff000000000000000000000000000000000000000081541690558551918252848201527f89b1add15eff56b3dfe299ad94e01f2b52fbcb80ae1a3baea6ae8c04cb2b98a4853392a2016104f9565b8280fd5b50346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610676816105ff6114a0565b936106086114c3565b6106106114e6565b73ffffffffffffffffffffffffffffffffffffffff968716835260016020908152848420928816845291825283832090871683528152919020549251938316845260a083901c65ffffffffffff169084015260d09190911c604083015281906060820190565b0390f35b50346103365760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336576106b26114a0565b906106bb6114c3565b916106c46114e6565b65ffffffffffff926064358481169081810361032a5779ffffffffffff0000000000000000000000000000000000000000947fda9fa7c1b00402c17d0161b249b1ab8bbec047c5a52207b9c112deffd817036b94338a5260016020527fffffffffffff0000000000000000000000000000000000000000000000000000858b209873ffffffffffffffffffffffffffffffffffffffff809416998a8d5260205283878d209b169a8b8d52602052868c209486156000146107a457504216925b8454921697889360a01b16911617179055815193845260208401523392a480f35b905092610783565b5082346105bf5760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576107e56114a0565b906107ee6114c3565b9265ffffffffffff604435818116939084810361032a57338852602091600183528489209673ffffffffffffffffffffffffffffffffffffffff80911697888b528452858a20981697888a5283528489205460d01c93848711156109175761ffff9085840316116108f05750907f55eb90d810e1700b35a8e7e25395ff7f2b2259abd7415ca2284dfb1c246418f393929133895260018252838920878a528252838920888a5282528389209079ffffffffffffffffffffffffffffffffffffffffffffffffffff7fffffffffffff000000000000000000000000000000000000000000000000000083549260d01b16911617905582519485528401523392a480f35b84517f24d35a26000000000000000000000000000000000000000000000000000000008152fd5b5084517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b503461033657807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610336578060209273ffffffffffffffffffffffffffffffffffffffff61098f6114a0565b1681528084528181206024358252845220549051908152f35b5082346105bf57817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf577f3704902f963766a4e561bbaab6e6cdc1b1dd12f6e9e99648da8843b3f46b918d90359160243533855284602052818520848652602052818520818154179055815193845260208401523392a280f35b8234610a9a5760807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc360112610a9a57610a606114a0565b610a686114c3565b610a706114e6565b6064359173ffffffffffffffffffffffffffffffffffffffff8316830361032e576102f6936117a1565b80fd5b503461033657817ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657602090610ad7611b1e565b9051908152f35b508290346105bf576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf57610b1a3661152a565b90807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c36011261033257610b4c611478565b9160e43567ffffffffffffffff8111610bda576102f694610b6f913691016115c1565b939092610b7c8351612054565b6020840151828501519083519260208401947f939c21a48a8dbe3a9a2404a1d46691e4d39f6583d6ec6b35714604c986d801068652840152336060840152608083015260a082015260a08152610bd18161141b565b51902091611c25565b8580fd5b509134610336576101007ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033657610c186114a0565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc360160c08112610332576080855191610c51836113e3565b1261033257845190610c6282611398565b73ffffffffffffffffffffffffffffffffffffffff91602435838116810361049f578152604435838116810361049f57602082015265ffffffffffff606435818116810361032a5788830152608435908116810361049f576060820152815260a435938285168503610bda576020820194855260c4359087830182815260e43567ffffffffffffffff811161032657610cfe90369084016115c1565b929093804211610d88575050918591610d786102f6999a610d7e95610d238851611fbe565b90898c511690519083519260208401947ff3841cd1ff0085026a6327b620b67997ce40f282c88a8e905a7a5626e310f3d086528401526060830152608082015260808152610d70816113ff565b519020611bd9565b916120c7565b519251169161199d565b602492508a51917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b5091346103365760607ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc93818536011261033257610df36114a0565b9260249081359267ffffffffffffffff9788851161032a578590853603011261049f578051978589018981108282111761104a578252848301358181116103265785019036602383011215610326578382013591610e50836115ef565b90610e5d85519283611437565b838252602093878584019160071b83010191368311611046578801905b828210610fe9575050508a526044610e93868801611509565b96838c01978852013594838b0191868352604435908111610fe557610ebb90369087016115c1565b959096804211610fba575050508998995151610ed681611ebb565b908b5b818110610f9757505092889492610d7892610f6497958351610f02816103f98682018095611f66565b5190209073ffffffffffffffffffffffffffffffffffffffff9a8b8b51169151928551948501957faf1b0d30d2cab0380e68f0689007e3254993c596f2fdd0aaa7f4d04f794408638752850152830152608082015260808152610d70816113ff565b51169082515192845b848110610f78578580f35b80610f918585610f8b600195875161175e565b5161199d565b01610f6d565b80610311610fac8e9f9e93610fb2945161175e565b51611fbe565b9b9a9b610ed9565b8551917fcd21db4f000000000000000000000000000000000000000000000000000000008352820152fd5b8a80fd5b6080823603126110465785608091885161100281611398565b61100b85611509565b8152611018838601611509565b838201526110278a8601611607565b8a8201528d611037818701611607565b90820152815201910190610e7a565b8c80fd5b84896041867f4e487b7100000000000000000000000000000000000000000000000000000000835252fd5b5082346105bf576101407ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc3601126105bf576110b03661152a565b91807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7c360112610332576110e2611478565b67ffffffffffffffff93906101043585811161049f5761110590369086016115c1565b90936101243596871161032a57611125610bd1966102f6983691016115c1565b969095825190611134826113ff565b606482527f5065726d69745769746e6573735472616e7366657246726f6d28546f6b656e5060208301527f65726d697373696f6e73207065726d69747465642c6164647265737320737065848301527f6e6465722c75696e74323536206e6f6e63652c75696e7432353620646561646c60608301527f696e652c0000000000000000000000000000000000000000000000000000000060808301528351948591816111e3602085018096611f93565b918237018b8152039361121c7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe095868101835282611437565b5190209261122a8651612054565b6020878101518589015195519182019687526040820192909252336060820152608081019190915260a081019390935260e43560c08401528260e081016102e1565b5082346105bf576020807ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc36011261033257813567ffffffffffffffff92838211610bda5736602383011215610bda5781013592831161032e576024906007368386831b8401011161049f57865b8581106112e5578780f35b80821b83019060807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffdc83360301126103265761139288876001946060835161132c81611398565b611368608461133c8d8601611509565b9485845261134c60448201611509565b809785015261135d60648201611509565b809885015201611509565b918291015273ffffffffffffffffffffffffffffffffffffffff80808093169516931691166117a1565b016112da565b6080810190811067ffffffffffffffff8211176113b457604052565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b6060810190811067ffffffffffffffff8211176113b457604052565b60a0810190811067ffffffffffffffff8211176113b457604052565b60c0810190811067ffffffffffffffff8211176113b457604052565b90601f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0910116810190811067ffffffffffffffff8211176113b457604052565b60c4359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b600080fd5b6004359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6024359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b6044359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b359073ffffffffffffffffffffffffffffffffffffffff8216820361149b57565b7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc01906080821261149b576040805190611563826113e3565b8082941261149b57805181810181811067ffffffffffffffff8211176113b457825260043573ffffffffffffffffffffffffffffffffffffffff8116810361149b578152602435602082015282526044356020830152606435910152565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020838186019501011161149b57565b67ffffffffffffffff81116113b45760051b60200190565b359065ffffffffffff8216820361149b57565b9181601f8401121561149b5782359167ffffffffffffffff831161149b576020808501948460061b01011161149b57565b91909160608184031261149b576040805191611666836113e3565b8294813567ffffffffffffffff9081811161149b57830182601f8201121561149b578035611693816115ef565b926116a087519485611437565b818452602094858086019360061b8501019381851161149b579086899897969594939201925b8484106116e3575050505050855280820135908501520135910152565b90919293949596978483031261149b578851908982019082821085831117611730578a928992845261171487611509565b81528287013583820152815201930191908897969594936116c6565b602460007f4e487b710000000000000000000000000000000000000000000000000000000081526041600452fd5b80518210156117725760209160051b010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b92919273ffffffffffffffffffffffffffffffffffffffff604060008284168152600160205282828220961695868252602052818120338252602052209485549565ffffffffffff8760a01c16804211611884575082871696838803611812575b5050611810955016926118b5565b565b878484161160001461184f57602488604051907ff96fb0710000000000000000000000000000000000000000000000000000000082526004820152fd5b7fffffffffffffffffffffffff000000000000000000000000000000000000000084846118109a031691161790553880611802565b602490604051907fd81b2f2e0000000000000000000000000000000000000000000000000000000082526004820152fd5b9060006064926020958295604051947f23b872dd0000000000000000000000000000000000000000000000000000000086526004860152602485015260448401525af13d15601f3d116001600051141617161561190e57565b60646040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601460248201527f5452414e534645525f46524f4d5f4641494c45440000000000000000000000006044820152fd5b91908110156117725760061b0190565b3573ffffffffffffffffffffffffffffffffffffffff8116810361149b5790565b9065ffffffffffff908160608401511673ffffffffffffffffffffffffffffffffffffffff908185511694826020820151169280866040809401511695169560009187835260016020528383208984526020528383209916988983526020528282209184835460d01c03611af5579185611ace94927fc6a377bfc4eb120024a8ac08eef205be16b817020812c73223e81d1bdb9708ec98979694508715600014611ad35779ffffffffffff00000000000000000000000000000000000000009042165b60a01b167fffffffffffff00000000000000000000000000000000000000000000000000006001860160d01b1617179055519384938491604091949373ffffffffffffffffffffffffffffffffffffffff606085019616845265ffffffffffff809216602085015216910152565b0390a4565b5079ffffffffffff000000000000000000000000000000000000000087611a60565b600484517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b467f0000000000000000000000000000000000000000000000000000000000007a6903611b69577fd5a17abc3865df5c1400c0299bd4ce2eefc8114aec5f9d3dded1745783e57b9890565b60405160208101907f8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a86682527f9ac997416e8ff9d2ff6bebeb7149f65cdae5e32e2b90440b566bb3044041d36a604082015246606082015230608082015260808152611bd3816113ff565b51902090565b611be1611b1e565b906040519060208201927f190100000000000000000000000000000000000000000000000000000000000084526022830152604282015260428152611bd381611398565b9192909360a435936040840151804211611cc65750602084510151808611611c955750918591610d78611c6594611c60602088015186611e47565b611bd9565b73ffffffffffffffffffffffffffffffffffffffff809151511692608435918216820361149b57611810936118b5565b602490604051907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b602490604051907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b959093958051519560409283830151804211611e175750848803611dee57611d2e918691610d7860209b611c608d88015186611e47565b60005b868110611d42575050505050505050565b611d4d81835161175e565b5188611d5a83878a61196c565b01359089810151808311611dbe575091818888886001968596611d84575b50505050505001611d31565b611db395611dad9273ffffffffffffffffffffffffffffffffffffffff6105159351169561196c565b916118b5565b803888888883611d78565b6024908651907f3728b83d0000000000000000000000000000000000000000000000000000000082526004820152fd5b600484517fff633a38000000000000000000000000000000000000000000000000000000008152fd5b6024908551907fcd21db4f0000000000000000000000000000000000000000000000000000000082526004820152fd5b9073ffffffffffffffffffffffffffffffffffffffff600160ff83161b9216600052600060205260406000209060081c6000526020526040600020818154188091551615611e9157565b60046040517f756688fe000000000000000000000000000000000000000000000000000000008152fd5b90611ec5826115ef565b611ed26040519182611437565b8281527fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0611f0082946115ef565b0190602036910137565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8114611f375760010190565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b805160208092019160005b828110611f7f575050505090565b835185529381019392810192600101611f71565b9081519160005b838110611fab575050016000815290565b8060208092840101518185015201611f9a565b60405160208101917f65626cad6cb96493bf6f5ebea28756c966f023ab9e8a83a7101849d5573b3678835273ffffffffffffffffffffffffffffffffffffffff8082511660408401526020820151166060830152606065ffffffffffff9182604082015116608085015201511660a082015260a0815260c0810181811067ffffffffffffffff8211176113b45760405251902090565b6040516020808201927f618358ac3db8dc274f0cd8829da7e234bd48cd73c4a740aede1adec9846d06a1845273ffffffffffffffffffffffffffffffffffffffff81511660408401520151606082015260608152611bd381611398565b919082604091031261149b576020823592013590565b6000843b61222e5750604182036121ac576120e4828201826120b1565b939092604010156117725760209360009360ff6040608095013560f81c5b60405194855216868401526040830152606082015282805260015afa156121a05773ffffffffffffffffffffffffffffffffffffffff806000511691821561217657160361214c57565b60046040517f815e1d64000000000000000000000000000000000000000000000000000000008152fd5b60046040517f8baa579f000000000000000000000000000000000000000000000000000000008152fd5b6040513d6000823e3d90fd5b60408203612204576121c0918101906120b1565b91601b7f7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff84169360ff1c019060ff8211611f375760209360009360ff608094612102565b60046040517f4be6321b000000000000000000000000000000000000000000000000000000008152fd5b929391601f928173ffffffffffffffffffffffffffffffffffffffff60646020957fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0604051988997889687947f1626ba7e000000000000000000000000000000000000000000000000000000009e8f8752600487015260406024870152816044870152868601378b85828601015201168101030192165afa9081156123a857829161232a575b507fffffffff000000000000000000000000000000000000000000000000000000009150160361230057565b60046040517fb0669cbc000000000000000000000000000000000000000000000000000000008152fd5b90506020813d82116123a0575b8161234460209383611437565b810103126103365751907fffffffff0000000000000000000000000000000000000000000000000000000082168203610a9a57507fffffffff0000000000000000000000000000000000000000000000000000000090386122d4565b3d9150612337565b6040513d84823e3d90fdfea164736f6c6343000811000a"; +}